Pagination
Cursor rules for list endpoints, and the list envelope.
Every list route answers the same envelope. data is the page, and pages.next.starting_after is an opaque cursor that names the next page, or null when the page was the last one.
{
"type": "list",
"data": [
{ "type": "conversation", "id": "…", "…": "…" }
],
"pages": {
"type": "pages",
"per_page": 20,
"next": { "starting_after": "eyJ2IjoxLCJzIjoibGFzdF9hY3Rpdml0eV9hdCIsIm8iOiJkZXNjIiwiayI6WyIyMDI2LTA4LTMxVDExOjAyOjAzLjQ1NloiLCJmNGY2ZmI2MC05YzExLTQzZTItOGJiMy02NTczMWE3ZmI2N2MiXX0" }
}
}Request parameters
per_pageis an integer from 1 to 150, default 20.starting_afteris the cursor from the previous page'snext.sortandordervary per entity; a cursor only works with the same sort and direction it was minted under.
Keyset, not offset
Pagination walks the sort key rather than an offset. Nothing is skipped when a row is inserted between pages, the way an OFFSET page would be. The tradeoff: a row whose sort key moves between pages can appear twice, and clients deduplicate on id.
The cursor is a base64url-encoded JSON object that pins the version, the sort, the direction and the last row's sort key plus id. A cursor minted under one sort but sent with another answers 400 invalid_cursor at the route, before any query runs.
Note
Some reads are not paginated. GET /api/conversations/counts returns one row per view, and the saved-replies and outbound collections cap at 150 because their screens walk the whole list.