> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brew.new/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Cursor-based pagination conventions for list endpoints in the Brew Public API v1 — limits, cursors, hasMore, and the standard iteration loop.

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

```ts theme={null}
type Pagination = {
  limit: number          // page size used for this response
  cursor?: string        // opaque token — pass back as `?cursor=…` to get the next page
  hasMore: boolean       // true when more rows exist
}
```

Where it appears (read mode):

```ts theme={null}
type ListResponse<Row> = {
  data: Array<Row>     // the rows for this page — always under `data`
  pagination?: Pagination
}
```

## Endpoints that paginate

Every list endpoint accepts `limit` (1–100) + `cursor` and returns the `pagination` envelope. The default `limit` is **100** except where noted.

| Endpoint                              | Default `limit` | Max `limit` | Notes                                                                                                                                                     |
| ------------------------------------- | --------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/contacts/search`            | 50              | 100         | The contact read. Filterable + sortable (`search`, `sort`, `order`, `logic`, `filters`, `audienceId`) in the JSON body.                                   |
| `GET /v1/fields`                      | 50              | 100         | Custom contact-field definitions for the brand.                                                                                                           |
| `GET /v1/automations/runs`            | 25              | 100         | Filterable on `automationId`, `triggerEventId`, `status`, `from`, `to`. Logs are attached per run.                                                        |
| `GET /v1/audiences`                   | 100             | 100         | Saved audiences for the brand.                                                                                                                            |
| `GET /v1/domains`                     | 100             | 100         | All sending domains; `?sendableOnly=true` for verified-only.                                                                                              |
| `GET /v1/emails`                      | 100             | 100         | Latest version per `emailId`. Filterable on `status`, `createdAtFrom/To`, `updatedAtFrom/To`.                                                             |
| `GET /v1/templates`                   | 100             | 100         | Public template catalog; each row is a **full** template (`html` + `previewImage` + title/category/brand). Filterable on `brand`, `category`, `semantic`. |
| `GET /v1/automations/triggers`        | 100             | 100         | Integration-provisioned + API-created triggers.                                                                                                           |
| `GET /v1/automations`                 | 100             | 100         | Latest version per `automationId`; **lean by default** (`?automationId=` + `?include=graph,versions` for one).                                            |
| `GET /v1/analytics/sends`             | 100             | 100         | Campaign sends + stats. Filterable on `status`, `emailId`, `from`, `to`; `?sendId=` + `?include=events` for one.                                          |
| `GET /v1/analytics/campaigns`         | 100             | 100         | Lifetime per-campaign KPIs.                                                                                                                               |
| `GET /v1/analytics/events`            | 50              | 100         | Unified event explorer. Filterable on `recipientEmail`, `eventType`, `automationId`, `from`, `to`.                                                        |
| `GET /v1/analytics/trigger-instances` | 100             | 100         | Fired-trigger audit log. Filterable on `triggerEventId`, `from`, `to`.                                                                                    |

## Single-resource reads (identity in the query)

Reads are flat: there is no separate get-one path. To fetch one row, pass
its id key to the resource's list endpoint and read `data[0]`. The
response is the **same `{ data, pagination }` envelope** as the list —
just scoped to one row. `?include=` opt-ins embed the heavier detail:

| Read                                                            | `include` opt-ins   |
| --------------------------------------------------------------- | ------------------- |
| `GET /v1/emails?emailId=eml_xxx` (carries `previewImage`)       | `html`, `versions`  |
| `GET /v1/audiences?audienceId=aud_xxx`                          | `count`             |
| `GET /v1/automations?automationId=auto_xxx`                     | `graph`, `versions` |
| `GET /v1/automations/runs?automationRunId=run_xxx`              | `logs`              |
| `GET /v1/automations/triggers?triggerEventId=tri_xxx`           | —                   |
| `GET /v1/analytics/sends?sendId=snd_xxx`                        | `events`            |
| `GET /v1/analytics/trigger-instances?triggerInstanceId=tin_xxx` | —                   |
| `GET /v1/domains?domainId=dom_xxx`                              | —                   |

When the id misses, you get an **empty `data` array** (and `404 <RESOURCE>_NOT_FOUND` only on the path-based write endpoints, e.g.
`PATCH /v1/automations/{automationId}`). The contact read is
`POST /v1/contacts/search` — look one address up with a
`{ field: 'email', operator: 'equals' }` filter.

## Canonical iteration loop

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):

```ts theme={null}
async function* paginateAllContacts() {
  let cursor: string | undefined
  while (true) {
    const res = await fetch('https://brew.new/api/v1/contacts/search', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.BREW_API_KEY!}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ limit: 100, ...(cursor ? { cursor } : {}) }),
    })
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
    const body = (await res.json()) as {
      data: Array<{ email: string }>
      pagination: { limit: number; cursor?: string; hasMore: boolean }
    }

    for (const contact of body.data) yield contact
    if (!body.pagination.hasMore) return
    cursor = body.pagination.cursor
  }
}

for await (const contact of paginateAllContacts()) {
  console.log(contact.email)
}
```

## SDK pagination (TypeScript)

The official `@brew.new/sdk` returns the raw `{ data, pagination }` shape — pass the cursor back on the next call:

```ts theme={null}
import { createBrewClient } from '@brew.new/sdk'

const brew = createBrewClient({ apiKey: process.env.BREW_API_KEY! })

let cursor: string | undefined
do {
  const page = await brew.contacts.search({ limit: 100, cursor })
  for (const contact of page.data) {
    console.log(contact.email)
  }
  cursor = page.pagination?.hasMore ? page.pagination.cursor : undefined
} while (cursor)
```

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).

## Filter combinations

`POST /v1/contacts/search` takes search + sort + filter in one JSON body:

```http theme={null}
POST /v1/contacts/search
{ "search": "jane", "sort": "createdAt", "order": "desc", "limit": 50 }

POST /v1/contacts/search
{ "logic": "and", "filters": [{ "field": "plan", "operator": "equals", "value": "pro" }], "limit": 100 }
```

`GET /v1/automations/runs` supports time-range and status filters:

```http theme={null}
GET /v1/automations/runs?automationId=auto_abc&status=completed&from=2026-04-01T00:00:00.000Z&to=2026-04-08T00:00:00.000Z&limit=100
```

See the per-endpoint pages under [Public API v1](/api-reference/public-v1/contacts/get-contacts) for the full filter parameter table.

## Cursor semantics

* **Opaque.** Cursors are server-generated tokens. Don't parse them; don't synthesise them. The format may change between releases.
* **Stable within a page.** A cursor returned on page N points to "the next batch of rows that existed when N was rendered". New rows inserted concurrently may show up; deleted rows may be skipped. This is fine for analytics / bulk export; if you need strict snapshot reads, freeze a time bound with `?from=&to=`.
* **24-hour TTL.** Cursors don't expire on a strict clock today, but treat them as if they're good for \~24h — re-start with no cursor if a job pauses overnight.

## See also

* [Rate limits](/api-reference/api/rate-limits) — a tight pagination loop can burn through `100/min` quickly; consider parallelizing across keys or honoring `X-RateLimit-Remaining`.
* [Batch operations](/api-reference/api/batch-operations) — for **writing** lots of rows fast (`POST /v1/contacts` accepts up to 1000 rows per request).
* [Errors](/api-reference/api/errors) — `404` on a get-one lookup; `400 INVALID_REQUEST` on bad cursors.

## 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:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to analyze documentation with ChatGPT or Claude for deeper insights.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
