Skip to content
Core Concepts

Pagination

Every list endpoint in kirim.dev uses the same opaque cursor contract — phone-scoped resources like GET /v1/{phone_number_id}/messages and root-level resources like GET /v1/labels behave identically.

GET /v1/106540352242922/messages?limit=25&cursor=eyJpZCI6Im1zZ18wMUhYWVoiLCJ0cyI6IjIwMjYtMDUtMjNUMTA6MDA6MDAuMDAwWiJ9 HTTP/1.1
Query paramDefaultRangeNotes
limit251–100Page size. Out-of-range → 400 invalid_limit.
cursoropaqueEcho next_cursor from the previous response, verbatim.

Resource-specific filters (status, conversation_id, direction, created_after, …) live alongside cursor — see each endpoint’s reference for the exact set.

{
"data": [ { "id": "msg_01HXYZ…" }, { "id": "msg_01HXYY…" } ],
"has_more": true,
"next_cursor": "eyJpZCI6Im1zZ18wMUhYWVoiLCJ0cyI6IjIwMjYtMDUtMjNUMTA6MDA6MDAuMDAwWiJ9",
"request_id": "req_01HXZ0ABCDEFGHJKMNPQRSTVWX"
}
FieldMeaning
dataArray of resources (zero or more).
has_moretrue if at least one more page is available.
next_cursorOpaque string to pass back on the next call. null when has_more is false.
request_idEchoes the X-Request-Id header.
import { Kirim } from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })
const phone = kirim.phoneNumbers('106540352242922')
// Async iterator — pages fetched lazily.
for await (const message of phone.messages.list({
conversation_id: 'cnv_01HXYZABCDEFGHJKMNPQRSTVWX',
limit: 50,
})) {
console.log(message.id, message.status)
}
// Or materialise (small lists only):
const labels = await kirim.labels.list().all()
  • Opaque. Do not parse, modify, or assume structure. The encoding may change at any time — the only promise is that the cursor you got back works on the next call.
  • Tamper-evident. A malformed or modified cursor returns 400 invalid_cursor.
  • Filter-bound. A cursor is only valid for the same filter set it was generated against. Changing status=open to status=resolved between pages invalidates the cursor — start a fresh request.
  • Stable under concurrent inserts. Keyset pagination on (created_at DESC, id DESC) means a row inserted while you’re paginating either appears on a page you’ve already seen (and is missing from later pages — accept this) or on a page you’ll see later, always with deterministic ordering.

Different resources have different natural orderings — pagination itself is the same.

EndpointOrder
GET /v1/{phone_number_id}/messages(created_at DESC, id DESC)
GET /v1/{phone_number_id}/contacts(created_at DESC, id DESC)
GET /v1/{phone_number_id}/conversations(last_message_at DESC NULLS LAST, id DESC)
GET /v1/{phone_number_id}/templates(created_at DESC, id DESC)
GET /v1/labels(created_at DESC, id DESC)
GET /v1/accounts(created_at DESC, id DESC)
GET /v1/webhook_subscriptions(created_at DESC, id DESC)
GET /v1/webhook_deliveries(created_at DESC, id DESC)

There is no ?sort= parameter — the default is the only order. A future ?sort=… parameter can be added non-breakingly with the current default still applying when unset.

  • Don’t ?limit=100 then drop most of the results client-side. Apply the resource-specific filters server-side; you save your read budget and reduce latency.
  • Don’t loop forever on next_cursor. A bug that forgets to pass cursor will paginate the first page repeatedly. Always exit when has_more is false.
  • Don’t paginate to materialise full tables for export. Use the dashboard’s CSV export feature instead — it streams server-side without burning your API quota.
  • Don’t change filters mid-iteration. Cursors are bound to the filter set. Restart with no cursor when filters change.

Resource IDs

The prefixed identifier scheme used in every page. Read →

Rate limits

Pagination shares your read budget. Read →

List messages

Filter set and ordering for the messages endpoint. API →

API reference

All list endpoints in one place. Browse →