Skip to content
TypeScript SDK

SDK Pagination

Every list() method on the SDK returns a Paginator<T>. It is both:

  • an async iterablefor await (const item of paginator) { ... }
  • a manual paginatorawait 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.

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
}

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`)

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)
}
  • 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 Promise
const paginator = await phone.messages.list({ limit: 50 })
// RIGHT — iterate it
for await (const msg of phone.messages.list({ limit: 50 })) { ... }
// RIGHT — call .page() to get a page
const 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 }))

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
}
  • 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.