Every list endpoint on this API returns the same envelope and pages the same way. There is one shape to parse and one helper per SDK that walks all of them.
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 for limit + 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. offset is 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.
If you need a stable snapshot of a busy list, filter it down to a closed window first: 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:
Both SDK helpers default to 200. Take the maximum for small rows — a call summary, an agent header, a phone number. Drop it for rows that carry text: a conversation item holds the full message body and its attachment metadata, and 500 of those is a large response to hold in memory for no benefit. There is no rate limit, so a smaller page costs you round trips and nothing else.

Walking every page

Each SDK ships one helper rather than a listAll per resource, because the envelope is identical everywhere.
Both take a 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.