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.
list_insights is the one list that trims instead of refusing. It shortens long text on each finding and cuts the report, suggestions, and memo where its budget ends. A truncated array names each cut section as { section, shown, total }: items shown of the total for report and suggestions, and characters of its markdown for memo. Every finding on the page stays, so the cursor still continues after the last one. The HTTP read always answers whole.
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, INSIGHT_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. Comment threads order by their latest message the same way, so a reply during the walk moves its thread to the front. - 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. - Insights refuse a stale cursor.
GET /v1/insightsranks its findings again on every request, and its cursor remembers which findings it already returned. A change below the cursor is served when its page comes. A finding added or removed above the cursor, or re-ranked across it, makes the next page400 INVALID_REQUESTwithparam: "cursor"rather than skip or repeat findings, so read again from the first page withoutcursor, with the samestateandseverity. A walk that reaches its last page has returned every finding exactly once, as the list stands then. A cursor sent with a differentstateorseverityis refused the same way. - A short page is not the last page.
GET /v1/notificationsreads a bounded slice of stored rows per page and leaves out the rows your key can’t see, so a page can hold fewer rows thanlimit, even none, whilehasMoreistrue. Stop whenhasMoreisfalse, never because a page came back short. Atypefilter your credential can’t see answers an empty page rather than an error. - A comment thread pages its own messages. With
include=messages, a thread whose older messages didn’t fit carries amessagesCursor. Send it back with that thread’scommentId(not withcursor) for the next older slice, until it comes backnull. See Comments. - 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.
Related
- 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.
Ask ChatGPT or Claude
Click here to open the Copy page menu at the top of this page, then send the page to ChatGPT, Claude, or your editor.