# Pagination

Every paginated list endpoint in the Bird API uses the same cursor-based contract: the same request parameters, the same response envelope, the same cursor semantics. Learn it once on [`GET /v1/email/messages`](/docs/api/reference/list-email-messages) and it applies everywhere.

A small number of bounded collections (for example, billing plans) return a plain `{"data": [...]}` array with no pagination fields at all — an endpoint either implements this contract in full or not at all, never partially.

## Request parameters

| Parameter        | Type    | Description                                                                                                                                                                                     |
| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`          | integer | Maximum items per page. Between `1` and `100`; defaults to `25`.                                                                                                                                |
| `starting_after` | string  | Cursor from the `next_cursor` field of a previous response. Returns items immediately after that position.                                                                                      |
| `ending_before`  | string  | Cursor from the `prev_cursor` field of a previous response. Returns items immediately before that position.                                                                                     |
| `include_total`  | boolean | When `true`, the response includes a `total` count. Defaults to `false`. Available on management endpoints only — high-volume data endpoints (messages, events, suppressions) do not accept it. |

Cursors are **opaque**: they are not resource IDs, they encode the sort position internally, and their format can change at any time. Receive them in responses and pass them back unchanged. A malformed or expired cursor returns a `422` with code `E01012 InvalidCursor` — restart pagination without a cursor.

Most list endpoints also accept resource-specific `sort` and `order` parameters; the per-endpoint reference documents the allowed sort fields. Changing the sort invalidates cursors from the previous sort order.

## Response envelope

```json
{
  "data": [{ "...": "..." }],
  "next_cursor": "WyIyMDI2LTA2LTEwVDA5OjE0OjAzWiIsICJtc2dfMDFr...",
  "prev_cursor": null,
  "refresh_cursor": "WyIyMDI2LTA2LTEwVDEyOjAwOjAwWiIsICJtc2dfMDFr...",
  "total": 1432
}
```

| Field            | Description                                                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`           | The page of items.                                                                                                                                                  |
| `next_cursor`    | Pass back as `starting_after` to fetch the next page. `null` when no next page exists — this is the loop-termination signal.                                        |
| `prev_cursor`    | Pass back as `ending_before` to step backward. `null` when no previous page exists (always `null` on the first page).                                               |
| `refresh_cursor` | A refresh anchor: save it, then pass it back as `ending_before` later to fetch items that have appeared since this response. Non-null whenever `data` is non-empty. |
| `total`          | Total items matching the request's filters across all pages. Present only when `include_total=true` was passed; otherwise `null`/absent.                            |

`next_cursor` and `prev_cursor` are independent — each is `null` exactly when its direction has no further page. There is no separate `has_more` flag; check `next_cursor` for `null`.

## Walking all pages with curl

```bash
CURSOR=""
while :; do
  RESP=$(curl -s "https://us1.platform.bird.com/v1/email/messages?limit=100${CURSOR:+&starting_after=$CURSOR}" \
    -H "Authorization: Bearer bk_us1_...")
  echo "$RESP" | jq -r '.data[].id'
  CURSOR=$(echo "$RESP" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done
```

The first request carries no cursor; every subsequent request passes the previous response's `next_cursor` as `starting_after`; the loop ends when `next_cursor` is `null`.

## Pagination in the SDKs

You rarely write that loop by hand. Every [Bird SDK](/docs/sdks/concepts) exposes list endpoints in two modes: lazy iteration that fetches pages transparently as you consume items, and a single-page accessor for manual cursor control.

<!-- bird:snippet email.list.iterate -->

```typescript
for await (const message of bird.email.list({ status: "bounced" })) {
  console.log(message.id);
}
```

<!-- bird:snippet email.list.iterate -->

```go
for msg, err := range client.Email.List(context.Background(), bird.EmailListParams{Status: bird.EmailStatusBounced}) {
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.Id)
}
```

<!-- bird:snippet email.list.iterate -->

```python
for message in client.email.list(status="delivered"):
    print(message.id)
```

## Rate limits

List endpoints share the `list` [rate-limit group](/docs/guides/rate-limits), separate from reads of individual resources and from writes — paginating through a large collection never consumes your send or management quota. The lazy iterators above issue one request per page, so a tight loop over a large collection draws down the `list` group at one request per `limit` items; use `limit=100` for bulk reads.

## Related

- [Email messages](/docs/api/reference/list-email-messages) — a representative paginated list endpoint
- [SDK concepts](/docs/sdks/concepts) — iteration and single-page accessors across the SDKs
- [Rate limits](/docs/guides/rate-limits) — groups, headers, and handling `429`s