Pagination
List operations return results in pages. This page explains the limit and cursor parameters, the list envelope and how cursors behave.
Parameters
| Parameter | Type | Meaning |
|---|---|---|
limit | integer, 1 to 100 | Most objects to return. Default 25. Outside the range: validation_error. |
cursor | string | The next_cursor of the previous page, exactly as returned. Omit it for the first page. |
Every list response has the same envelope:
JSON
| Field | Meaning |
|---|---|
data | The objects on this page (full objects, as from the single-object read). |
has_more | true when more objects exist after this page. |
next_cursor | Pass as cursor to get the next page. null on the last page. |
(Objects in this example are shortened, and the cursor value is illustrative.)
Order
Most lists are newest first: by created_at descending, then by id. A few lists have a different, stable order:
- GET
/phone-numbers: by number. - GET
/campaigns/{campaign_id}/recipients: in a stable order that is not by time and has no other meaning. - GET
/dnc-entries: in test mode newest entry first; in live mode by contact, newest first.
Objects created while you page through a newest-first list appear at the start, so they are not on later pages: start a new listing (or use created_after) to pick them up.
Iterating
Request pages until has_more is false, passing each page's next_cursor as cursor and keeping every filter the same. A page can hold fewer than limit objects while has_more is still true (this happens when you filter calls by status), so always use has_more, never the page size, to decide when to stop.
The first page (here one call per page, to keep the example short):
JSON
has_more is true, so request the next page with cursor set to next_cursor, exactly as returned, and the same filters (limit may change):
JSON
has_more is false and next_cursor is null: this was the last page. (The cursor in these examples is illustrative; real cursors are opaque.)
Each page request counts against the read rate limit. Use limit=100 and filters to read fewer pages.
The complete runnable example reads every page of ended calls from the last 24 hours, with the same filters on every page, and counts them by outcome.
Complete runnable example
Read every page of a list: follow next_cursor, with the same filters, until has_more is false.
The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.
- cURL script (bash)
pagination.sh - JavaScript (Node.js 20+)
pagination.mjs - Python 3.9+ (requests)
pagination.py
Cursor rules
- Cursors are opaque: do not decode, edit, build or shorten them. They are URL-safe strings of up to 512 characters.
- A cursor is bound to the list and the filters that produced it, and to your workspace and mode. Send it only to the same list (for recipients and deliveries, the same campaign or endpoint id in the path) with exactly the same filters;
limitmay change between pages. - A cursor that was modified or is sent with different filters answers
invalid_cursor(400,retry: no); one longer than 512 characters answersvalidation_error. Start again without a cursor. - Use a cursor to continue the listing you are reading now, not as a long-lived bookmark. To resume a sync later, store the
created_atof the newest object you processed and list again withcreated_after.
To change filters, start a new listing from the first page.
Errors
| Code | Cause | Fix |
|---|---|---|
invalid_cursor | Edited or truncated cursor, a cursor from another list, or filters changed between pages | Send next_cursor exactly as returned with the same filters, or start again without a cursor. |
validation_error | limit below 1 or above 100, an unknown query parameter, or a parameter given twice | Fix the query string. details.errors names the parameter. |