pagination: { limit, cursor, hasMore } envelope so callers can iterate without page-counting math.
The Pagination Envelope
Endpoints That Paginate
Every list endpoint acceptslimit (1-100) + cursor and returns the pagination envelope. The default limit is 100 except where noted.
These are the HTTP limits. The MCP tools read smaller pages; see MCP page sizes.
MCP Page Sizes
An MCP tool result must fit the host’s response budget. A list page that doesn’t fit is refused whole withRESPONSE_TOO_LARGE: no rows and no cursor, so repeating the call with the same cursor and a smaller limit skips nothing. The MCP tools therefore page in smaller steps than HTTP:
A contact carries every custom field it has, so a
search_contacts page is as wide as your brand’s field list. Raise limit toward 25 only when your contacts carry few custom fields.
Single-Resource Reads (Identity in the Path)
Every collection has a real detail read at/{collection}/{id}. It returns
the bare row, not a list envelope, so there is no data[0] unwrap and no
pagination object. ?include= opt-ins embed the heavier detail:
When the id misses, each detail read returns a resource-specific
404:
EMAIL_NOT_FOUND, AUDIENCE_NOT_FOUND, SEND_NOT_FOUND, DOMAIN_NOT_FOUND,
AUTOMATION_RUN_NOT_FOUND, TRIGGER_INSTANCE_NOT_FOUND, CONTACT_NOT_FOUND,
FLOW_NOT_FOUND. Check the endpoint in the generated reference for its exact
code, and see Errors for the envelope.
Canonical Iteration Loop
This is the standard cursor pattern. The contact read is aPOST with
a JSON body, so the cursor rides in the body (GET lists put cursor
in the query instead):
SDK Pagination (TypeScript)
The official@brew.new/sdk returns the raw { data, pagination } shape. Pass the cursor back on the next call:
brew.contacts.search → brew.automations.runs.list with the same shape (runs use a GET list, so its filters are query params). To read one run, call brew.automations.runs.get(automationRunId) instead: a detail read is not a one-row page.
Filter Combinations
POST /v1/contacts/search takes search + sort + filter in one JSON body:
GET /v1/automations/runs supports time-range and status filters:
Cursor Semantics
- Opaque. Cursors are server-generated tokens. Don’t parse them; don’t synthesize them. The format may change between releases.
- Safe to change the list while you walk it. A cursor names the last row it returned, not a position, and the next page is the rows that sort after it. Deleting the rows you have read (for example, every audience that matches a search, page by page) skips nothing, and a row created during the walk is never returned twice. A row created where the walk has already passed is not returned; start a new walk to see it.
- Sort by a field the walk doesn’t change. Lists ordered by
updatedAt(emails by default, automations, triggers) move a row to the front when it is edited, so a row edited ahead of the cursor during your walk moves behind it. For a walk that edits what it reads, page emails withsortBy=createdAt, or collect the ids first and then act on them. - Catalogs page by position.
GET /v1/fields,GET /v1/flows,GET /v1/templatesandGET /v1/integrationspage by offset. Deleting custom fields mid-walk shifts their pages, so collect first, then act. - Brand images page two ways. Browsing
GET /v1/brand/imageswithoutqcontinues after the last asset the previous page returned, so uploads and deletes during a walk don’t repeat or skip an asset. A browse cursor keeps itssortandkind: sending it with a different one is a400 INVALID_REQUEST. A search withqpages its ranked results by position, and its cursor is signed: it only continues the sameqandkind, and any other pair is a400 INVALID_REQUEST. - 24-hour TTL. Cursors don’t expire on a strict clock today, but treat them as if they’re good for ~24h, and re-start with no cursor if a job pauses overnight.
See Also
- Rate limits: a tight pagination loop can burn through
100/minquickly; consider parallelizing across keys or honoringX-RateLimit-Remaining. - Batch operations: for writing lots of rows fast (
POST /v1/contactsaccepts up to 1000 rows per request). - Errors:
404on a get-one lookup;400 INVALID_REQUESTon bad cursors or a retired id filter. - Migrating to the cleaned-up v1 surface: every route, field, and status rename in one grep-able table.
Need Help?
Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:- Self-Service Tools
- Talk to Our Team
Search Documentation
Type in the “Ask any question” search bar at the top left to instantly find relevant documentation pages.
ChatGPT/Claude Integration
Click “Open in ChatGPT” at the top right of any page to explore it further with ChatGPT or Claude.