PaginationMenu
On this page

Pagination

Updated

Page through any list with an opaque cursor from meta.pagination.nextCursor; there is no page parameter.

Every list endpoint is cursor-paginated, except a few short ones that return everything in one response: listContactTopics, listStandardEventNames, listCampaignLinks, listSenders and listWebhookEvents. Use the returned cursor to request the next page.

The parameters

Parameter Description
limit Items per page. Default 20, maximum 100. Some endpoints cap lower; the reference gives the real maximum per operation.
cursor Opaque position marker. Omit for the first page.

Filters are ordinary query parameters beside these — query (search text, at least 1 character) on the lists that take it, plus per-endpoint ones such as status. The longest query differs by list; see Limits. Keep every filter the same across a traversal.

Date ranges

Every date range in the API is half-open: the start is inclusive and the end exclusive, so consecutive ranges never overlap or leave a gap. Both bounds are ISO 8601.

  • Filters on a list’s items are named for the field they filter, <field>After (at or after) and <field>Before (before): createdAfter and createdBefore on contacts and messages, occurredAfter and occurredBefore on the event log. Each takes a date-time with an offset.
  • Report windows, the period a report or chart aggregates, are from (inclusive) and to (exclusive) on every report, the campaign series and journey statistics. Each takes a date-time, or a date (YYYY-MM-DD) meaning the start of that day in the workspace timezone. To include a whole last day, pass the day after it:
Code
GET /v1/reports/overview?from=2026-10-01&to=2026-10-08     1 to 7 October

A report’s response gives the window it covered as from and to instants, with the timezone its days are in. Omit both for the last 30 days. A report window ends no later than the end of today and touches at most 366 days; a series covers at most 72 hours or 366 days of buckets.

The response

JSON excerpt
// A list response; "data" holds the page's items
{
  "data": [ … ],
  "meta": { "pagination": { "limit": 20, "nextCursor": "eyJpZCI6IjAxOGYi…" } }
}

Pass nextCursor back as cursor to get the next page. nextCursor is null on the last page — that, not an empty data array, is the signal to stop. A full page can still be the last one.

Code
GET /v1/contacts?limit=50
GET /v1/contacts?limit=50&cursor=eyJpZCI6IjAxOGYi…
GET /v1/contacts?limit=50&cursor=eyJpZCI6IjAxOGYj…   → nextCursor: null

There is no total, except on contacts and segment contacts

Lists do not report how many items there are, and that is deliberate. An exact count costs a scan of every matching row on every page — cheap for fifty campaigns, not for a million contacts or a year of messages — and it goes stale the moment a row is written, so a cursor walk and a total can disagree even when both are correct. Only the counts that answer a question in their own right exist: a segment’s memberCount, kept by its background evaluation, and the stats endpoints for delivery.

A client that wants to show a size can say what it knows for certain: the items on the page it holds, and — when nextCursor is null on the first page — that those are all of them.

The one exception is GET /v1/contacts, because the contact list is where teams ask “how many match?”: its first page carries meta.count, the contacts matching the filters counted up to 10,000, and meta.countExact, false when the count stopped at that cap. The count stops at the cap so it stays cheap however large the workspace, and both are null on later pages (any request with a cursor), since they do not change between pages.

GET /v1/segments/{id}/contacts carries the same meta.count and meta.countExact: the matching contacts counted up to 10,000, on the first page only. A list carries no count of its own: GET /v1/contacts?listId= counts its members the same way.

The cursor is opaque

It is base64url-encoded JSON, but that is an implementation detail and not part of the contract: do not decode it, construct one, derive a page number from it, or persist one as a durable bookmark. A malformed cursor returns 400 VALIDATION_ERROR.

A cursor belongs to the filters it was issued under: every query parameter except cursor and limit. Sent with different filters — another status, query or date range — it returns 400 VALIDATION_ERROR with an issue at cursor (in: "query"), rather than a page of the wrong list. Start again without cursor when the filters change.

Keep limit the same across a run. Changing it mid-traversal is not an error, but the cursor encodes position, not an offset, so mixing sizes makes the result harder to reason about than it looks.

Consistency

Lists are read from live data. A row inserted or deleted while you are part-way through a traversal may be missed or repeated across page boundaries; cursor pagination avoids the large-scale repeats that offset pagination causes, but it is not a snapshot.

Segment listings deserve particular care. GET /v1/segments/{id}/contacts evaluates the segment on read, so it can return a different population between pages if contacts are changing underneath. GET /v1/contacts?segmentId= reads the segment’s members as of its last background sweep, so it can lag behind recent changes. If you need a stable audience, use a list.

Exporting

To take the whole send log as a file, see Message log and exports.

Next steps