Skip to main content
Brew list endpoints use opaque cursor pagination. Every paginated response carries a pagination: { limit, cursor, hasMore } envelope so callers can iterate without page-counting math.

The Pagination Envelope

Where it appears (read mode):

Endpoints That Paginate

Every list endpoint accepts limit (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 with RESPONSE_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:
Passing an id as a list filter is retired. GET /v1/emails?emailId=, ?audienceId=, ?automationId=, ?automationRunId=, ?triggerEventId=, ?sendId=, ?triggerInstanceId= and ?domainId= are no longer accepted query keys and return 400 INVALID_REQUEST with param naming the key. Use the path read above. The one place an id still filters a list is a deliberate join: GET /v1/sends accepts automationId, automationRunId, audienceRunId or triggerInstanceId to list the per-recipient sends that run produced, and GET /v1/automations/runs accepts automationId, triggerEventId or triggerInstanceId for the same reason.
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 a POST 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:
For automation runs, swap 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:
See the per-endpoint pages under Public API v1 for the full filter parameter table.

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 with sortBy=createdAt, or collect the ids first and then act on them.
  • Catalogs page by position. GET /v1/fields, GET /v1/flows, GET /v1/templates and GET /v1/integrations page by offset. Deleting custom fields mid-walk shifts their pages, so collect first, then act.
  • Brand images page two ways. Browsing GET /v1/brand/images without q continues 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 its sort and kind: sending it with a different one is a 400 INVALID_REQUEST. A search with q pages its ranked results by position, and its cursor is signed: it only continues the same q and kind, and any other pair is a 400 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/min quickly; consider parallelizing across keys or honoring X-RateLimit-Remaining.
  • Batch operations: for writing lots of rows fast (POST /v1/contacts accepts up to 1000 rows per request).
  • Errors: 404 on a get-one lookup; 400 INVALID_REQUEST on 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:

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.