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
| Method | Returns | Use when |
|---|---|---|
list(query) | T[] — one page | You 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
limitcaps rows per page (the API applies its own ceiling). It does not cap the totallistAllyields — break out of the loop for that.- Cancellation propagates: pass
{ signal }and every page request honours it. Iteration stops with aMailroomConnectionError. - 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.