All four are always present. Paging is
limit and offset as query parameters:
There is no total and no cursor
A page is built by asking the database forlimit + 1 rows and trimming the
extra one. has_more is whether that extra row came back. Nothing ever runs a
count(*) over the table, which is what keeps a list of a hundred thousand
calls as fast as a list of ten.
The cost of that is exactly what you would expect:
- No total. You cannot render “page 3 of 40” or “1,284 calls” without walking the list to the end. There is no field to read it from and no parameter that asks for one.
- No cursor.
offsetis the only way forward. Most lists are ordered newest first — agents and tools by last edit, calls and conversations by start — so a row created while you are paging shifts every later row down by one, and a long walk can hand you the same item twice or step over one. The two append-only lists, conversation items and knowledge-base pages, are ordered oldest first and are stable to walk forward.
GET /v1/calls takes start and end, and a window that has already
ended cannot grow underneath you.
Choosing a page size
limit is capped per endpoint. On all but two the ceiling is 200, which is
also the default on most of them; GET /v1/conversations/{conversation_id}/items
and GET /v1/knowledge/{kb_id}/pages allow 500. A limit above the ceiling
is a 422, not a silent clamp:
Walking every page
Each SDK ships one helper rather than alistAll per resource, because the
envelope is identical everywhere.
limit of their own — paginate(fn, { limit: 50 }) in TypeScript,
paginate(fn, limit=50) in Python — which is the page size they request, not a
ceiling on how many items they yield. They stop when has_more is false.
paginate_async is the awaited form in Python, driven with async for. In
TypeScript paginate is already an async generator and works against either
client.
Which endpoints page
Twenty-five endpoints return a page. The SDK method names below are the TypeScript spelling; Python is the same path in snake case (telephony.phone_numbers.list).
Everything else answers whole.
GET /v1/catalog,
GET /v1/integrations/{integration_id}/mcp-tools, GET /v1/email/senders and
GET /v1/observability take no paging parameters at all — they are bounded by
what the workspace configured, not by how much traffic it has run.
The two voice and avatar catalogs are the exception
GET /v1/catalog/voices and GET /v1/catalog/avatars page, but not in this
envelope, because they are reading a provider’s library rather than a table of
yours. They carry their own filter facets alongside the results, so neither is a
Page and neither works with the SDK helpers.
Walk those with
has_more by hand. Most providers hand back their whole voice
list in one response and answer has_more: false on the first request;
total_count is the one count anywhere in this API, and it is nullable because
a provider does not always report one.