Skip to main content

Pagination

Mailroom paginates with opaque cursors, not page numbers. Two resources are cursor-paginated: contacts and the send log.

pagination.ts
import { collect } from "@mailroom/sdk";
import { mailroom } from "./client";

// 1. One page, simplest form — the API's default page size.
const firstPage = await mailroom.contacts.list({ status: "subscribed", limit: 100 });

// 2. Manual cursor paging when you control the loop (e.g. a job with a budget).
let cursor: string | undefined;
do {
const page = await mailroom.contacts.listPage({ status: "subscribed", cursor });
console.log(page.data.length);
cursor = page.nextCursor ?? undefined;
} while (cursor);

// 3. Auto-paginating iterator — walks every page for you.
for await (const contact of mailroom.contacts.listAll({ tag: "beta" })) {
console.log(contact.email);
}

// 4. `collect` drains an iterator into an array. Only for small result sets:
// it holds every row in memory.
const betaContacts = await collect(mailroom.contacts.listAll({ tag: "beta" }));

console.log(firstPage.length, betaContacts.length);

Which method

MethodReturnsUse when
list(query)T[] — one pageYou want the first N rows and nothing more
listPage(query){ data, nextCursor }You control the loop (checkpointing, budgets, a job that resumes)
listAll(query)AsyncGenerator<T>You want every match and can stream it
const page = await mailroom.contacts.listPage({ status: "subscribed", cursor });
page.data; // rows
page.nextCursor; // string, or null when exhausted

Pass the previous nextCursor back as cursor to continue. Keep the other filters identical between calls — a cursor is only meaningful for the query that produced it. Treat the cursor as an opaque token: never parse or construct one.

Helpers

collect() drains any async iterable into an array. Convenient, but it holds every row in memory — fine for a few thousand contacts, wrong for a full list export:

import { collect } from "@mailroom/sdk";
const beta = await collect(mailroom.contacts.listAll({ tag: "beta" }));

paginate() is the generic engine behind listAll — reach for it only if you are wrapping a cursor endpoint the SDK does not expose yet:

import { paginate } from "@mailroom/sdk";
for await (const row of paginate((cursor) => fetchMyPage(cursor))) { }

Practical notes

  • limit caps rows per page (the API applies its own ceiling). It does not cap the total listAll yields — break out of the loop for that.
  • Cancellation propagates: pass { signal } and every page request honours it. Iteration stops with a MailroomConnectionError.
  • Live data shifts. Rows created mid-walk may or may not appear. For an exact snapshot, filter on a fixed window instead of assuming the list is frozen.
  • Retries are per page. A failed page throws where you are iterating; the pages you already consumed stay consumed, so make your loop body idempotent if you plan to restart from a checkpoint.

Reference