- The default and maximum page size differ per endpoint. Most are 50/100. Profiles and campaigns are 25/200.
- The
pageparameter is accepted on two endpoints and rejected on all the others. Sending it where it is not supported returns400, because the API rejects unknown query properties.
Response shape
Paginated results are thedata 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.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.
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
Follownext_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.Sort ascending on a column that does not change
Sort ascending on a column that does not change
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.Treat updatedAt ordering as unstable
Treat updatedAt ordering as unstable
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.Do not rely on order within equal sort values
Do not rely on order within equal sort values
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.Expect total to move during a scan
Expect total to move during a scan
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.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 foritems, 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.

