SDK Pagination
Every list() method on the SDK returns a Paginator<T>. It is both:
- an async iterable —
for await (const item of paginator) { ... } - a manual paginator —
await paginator.page(cursor)returns one page at a time
The same instance works either way; pick whichever fits your control flow. Default page size is 25, max 100.
Auto-iterate every page
Section titled “Auto-iterate every page”The most ergonomic shape — exhaust every page transparently:
import { Kirim } from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })const phone = kirim.phoneNumbers('106540352242922')
for await (const msg of phone.messages.list({ direction: 'outbound' })) { console.log(msg.id, msg.status, msg.created_at)}The iterator fetches the first page lazily, yields each item, then
auto-fetches the next page until next_cursor is null. Break early
to stop pagination — no extra HTTP calls happen after break.
const first100: Message[] = []for await (const msg of phone.messages.list({ limit: 50 })) { first100.push(msg) if (first100.length >= 100) break // stops after 2 pages, cleanly}Step page-by-page
Section titled “Step page-by-page”When you need to persist cursors (resumable sync jobs, UI-driven “Load more” buttons), step manually:
const paginator = phone.messages.list({ limit: 50 })const page1 = await paginator.page()// page1.data → Message[]// page1.hasMore → boolean// page1.nextCursor → string | null// page1.requestId → string
console.log(page1.data.length, page1.hasMore)
if (page1.hasMore) { const page2 = await paginator.page(page1.nextCursor) // ...}Real-world example: find all failed messages
Section titled “Real-world example: find all failed messages”Filter on the API side, iterate everything that matches:
const failedIds: string[] = []
for await (const msg of phone.messages.list({ status: 'failed', created_after: '2026-05-01T00:00:00Z', limit: 100,})) { failedIds.push(msg.id)}
console.log(`${failedIds.length} failed messages in May`)Resumable sync pattern
Section titled “Resumable sync pattern”Persist the cursor between job runs so you only fetch what’s new:
async function syncMessages() { const lastCursor = await db.getKey('kirim:messages:cursor')
const paginator = phone.messages.list({ limit: 100 }) let page = await paginator.page(lastCursor)
while (true) { for (const msg of page.data) { await upsertMessage(msg) } if (!page.hasMore) break page = await paginator.page(page.nextCursor) }
// Save the final cursor for the next run await db.setKey('kirim:messages:cursor', page.nextCursor)}Cursor stability rules
Section titled “Cursor stability rules”- Stable across pages. A cursor returned by page N always points at the start of page N+1, even hours later.
- Stable across deploys. Cursors do not embed schema versions.
- Forward-only. There is no
previousCursor. To go back, restart from the beginning with the same filters. - Filter-bound. A cursor encodes the filters from the original call. Don’t reuse a cursor with different filters — paginate from scratch instead.
Gotcha: don’t await the paginator itself
Section titled “Gotcha: don’t await the paginator itself”Paginator<T> is not a Promise. It’s an async iterable. Awaiting
it directly does nothing useful — you’ll get back the same paginator
object wrapped in a resolved promise.
// WRONG — `paginator` is a Paginator, not a Promiseconst paginator = await phone.messages.list({ limit: 50 })
// RIGHT — iterate itfor await (const msg of phone.messages.list({ limit: 50 })) { ... }
// RIGHT — call .page() to get a pageconst page = await phone.messages.list({ limit: 50 }).page()If you want a plain array of everything (small result sets only —
think small lookup tables), use Array.fromAsync:
const all = await Array.fromAsync(phone.contacts.list({ limit: 100 }))Forwarding request options
Section titled “Forwarding request options”Per-call options (timeout / retry / abort signal) flow through to every page request:
const ac = new AbortController()setTimeout(() => ac.abort(), 5_000)
try { for await (const c of phone.contacts.list({}, { signal: ac.signal })) { process(c) }} catch (err) { // err is a ConnectionError if the signal fired}What you cannot do
Section titled “What you cannot do”- Rewind. There is no
previousCursor. Restart from the beginning if you need to go back. - Get a total count. The API does not return a total. Count by iterating, or query a pre-aggregated dashboard.
- Mutate while iterating. Like any cursor-paginated API, mutations between page fetches can cause items to be repeated or skipped. Snapshot before mutating, or use stricter filters.
Related
Section titled “Related”- Concepts → Pagination — the underlying cursor protocol.
- SDK Errors — how the SDK surfaces failures mid-iteration.