Skip to main content
Every paginated list endpoint in the public API returns the same envelope. The shape fits no-code tools like Make, which expect a page size, an offset and a has-more flag. Two things are not uniform and are the usual source of surprise:
  • The default and maximum page size differ per endpoint. Most are 50/100. Profiles and campaigns are 25/200.
  • The page parameter is accepted on two endpoints and rejected on all the others. Sending it where it is not supported returns 400, because the API rejects unknown query properties.
Both are spelled out in the limits table below.

Response shape

Paginated results are the data value of the standard envelope:
array
required
The page of records. Empty when the offset is past the end of the collection.
integer
required
The page size that was applied, after defaults.
integer
required
The offset that was applied.
integer
required
Total records matching the request, ignoring limit and offset.
boolean
required
true when offset + items.length < total.
string | null
required
When has_more is true, the offset to request next, as a string. null on the last page.The value is offset + limit, not offset + items.length. A short page still advances by a full page width, so do not compute the next offset yourself from the number of items you received.
next_cursor is a stringified numeric offset, not an opaque token. Feed it back as offset. Feeding it back as page either 400s or silently jumps to the wrong place.

Query parameters

Two parameter families exist. Which one applies is per endpoint.

Standard page

Used by lists, templates, custom fields, messages, tags, sender addresses, SMS sender IDs and events.
integer
default:"50"
Page size. Minimum 1, maximum 100. A value outside the range is rejected with 400, it is not clamped.
integer
default:"0"
Number of records to skip. Minimum 0.
These endpoints do not accept page. Because the API rejects unknown properties, ?page=2 returns 400 with the message property page should not exist.

Profiles and campaigns

GET /public/profiles and GET /public/campaigns carry richer filters and a different, larger page budget.
integer
default:"25"
Page size. Minimum 1, maximum 200. A value outside the range is rejected with 400.
integer
Number of records to skip. Minimum 0. When present it wins over page.
integer
default:"1"
1-based page number, an alternative to offset. Minimum 1. Ignored if offset is also present in the same request.
string
Sort column, optionally with a direction: -createdAt (descending), createdAt:asc, or a bare createdAt. A bare timestamp column defaults to descending, any other bare column to ascending. An unrecognised column is ignored and the endpoint’s default sort applies, with no error.Profiles sort on createdAt, updatedAt, email, firstName or lastName. Campaigns sort on createdAt, updatedAt, name, scheduleAt or sentAt.
Mixing page and offset in the same request is not an error. offset is applied and page is discarded. Pick one and stay with it.

Per-endpoint limits

Paging through results

Follow next_cursor until it is null. Do not stop on a short page: a page can come back shorter than limit and still have more behind it.

Paging reliably

Offset paging reads a moving collection. Records created, deleted or edited between two page requests shift the window, so a long scan can skip a record or return one twice. Four rules keep that under control.
A full scan of profiles should use sort=createdAt:asc. createdAt is never rewritten, and ascending order means new records land after your cursor rather than in front of it. The default descending order does the opposite: every new profile pushes the whole collection down by one and you re-read a record you already have.Sorting is only available on profiles and campaigns. Every other endpoint has a fixed order.
GET /public/lists, GET /public/templates and GET /public/campaigns order by updatedAt descending by default, and updatedAt moves whenever the record is touched. Editing a template mid-scan moves it to page 1 and shifts everything behind it. For campaigns, pass sort=createdAt:asc. For lists and templates the order is not configurable, so keep the scan short by requesting the maximum page size, and re-read rather than assuming a single pass was complete.
Sorting is applied on a single column with no secondary tiebreaker. Two records sharing a createdAt value can come back in either order, and can swap between requests. If you deduplicate by id while collecting, this never matters.
total is counted per request against the live collection. It is accurate at the moment the page was built and is not a snapshot pinned for the duration of your paging loop. Drive your loop from has_more, not from a total you captured on the first page.

Polling for new records

Do not re-scan a whole collection on a schedule to find what changed. Two better options:

Subscribe to webhooks

Events such as contact.created, contact.updated and the message engagement events are pushed to you as they happen. This is the correct mechanism for change detection, and it costs no polling budget.

Poll a narrow window

If you must poll, request the first page only, ordered newest first, with a page size a little larger than the volume you expect between runs. Stop as soon as you reach a record you already hold, and keep the last seen id rather than an offset.
Polling counts against the rate limits on Errors and rate limits. A scan that produces 200 pages costs 200 requests, which exhausts the per-minute IP bucket long before it touches the per-key one. Space a large scan out, and back off on 429.
GET /public/tags, GET /public/sender-addresses, GET /public/sms-sender-ids and GET /public/events build the whole collection in memory and slice the requested page out of it. total is exact and paging behaves normally, but every request does the same full read, so there is nothing to gain from a small page size on those four. Request them in one page.

Responses that are not paginated

Three responses in the public API do not carry the pagination envelope. Do not look for items, has_more or next_cursor on them. Every other single-record endpoint (GET /public/me, GET /public/profiles/:id, GET /public/messages/:id and the profile writes) returns one object as data.

Errors

An offset past the end of the collection is not an error. It returns an empty items array with the real total, has_more: false and next_cursor: null.