Skip to content

Pagination

List endpoints return a cursor, not a page number:

{
"data": [ /* ... */ ],
"has_more": true,
"next_cursor": "MTc1NTY0ODAwMDAwMDAwMDAwMC42ZjFjMmQzZS00YTVi..."
}

To get the next page, pass it back:

GET /v1/contacts?limit=100&cursor=MTc1NTY0ODAwMDAwMDAwMDAwMC42ZjFjMmQzZS00YTVi...

Stop when has_more is false. When it is false, next_cursor is absent.

These tables take inserts constantly. Between fetching page 1 and page 2 the head of the list moves, so a page-numbered walk both repeats rows and skips them, and neither is visible from the outside. A cursor is anchored to the exact row it was produced from, so the walk stays consistent no matter what arrives while you are reading.

async function* allContacts(apiKey) {
let cursor = null
do {
const url = new URL('https://k-message.kerneltics.com/v1/contacts')
url.searchParams.set('limit', '100')
if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } })
if (!res.ok) throw new Error(`contacts: ${res.status}`)
const page = await res.json()
yield* page.data
cursor = page.has_more ? page.next_cursor : null
} while (cursor)
}

limit defaults to 50 and caps at 200. A larger value is clamped rather than rejected: asking for 1000 means “as many as possible”, and failing the request teaches you nothing a clamp does not.

It is a token to hand back, nothing more. Do not parse it or construct one. The internal ordering behind it can change without notice, and code that decoded it would break when it does.

A malformed cursor returns 422 rather than silently starting from the beginning, because a paging loop that quietly resets never terminates and never reports anything wrong.

Filters combine with cursors. Keep them identical across every request in one walk; changing a filter mid-walk gives a cursor from one result set to a different one, and the results will not be what you expect.

The most useful filter is updated_since on contacts, which is what makes a nightly sync possible without pulling everything:

GET /v1/contacts?updated_since=2026-08-19T00:00:00Z&limit=200