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

# Pagination

> Cursor and page pagination on Developer API list endpoints.

List endpoints (`GET /developer/vouchers`, `/customers`, `/products`) accept
**two pagination shapes** on the same endpoint. Pick whichever fits your
integration; if both are supplied on the same request, the cursor shape wins.

## Cursor pagination (recommended)

Pass `cursor` and `limit`. The response includes `next_cursor` (`null` at
the end of the walk).

```http theme={null}
GET /api/v1/developer/vouchers?limit=50
```

```json theme={null}
{
  "vouchers": [/* …items… */],
  "next_cursor": "cur_v1_eyJ2IjoxLCJ0Ijoi…uH8.QwR2…"
}
```

Continue walking by passing the `next_cursor` value verbatim:

```http theme={null}
GET /api/v1/developer/vouchers?cursor=cur_v1_eyJ2IjoxLCJ0Ijoi…uH8.QwR2…&limit=50
```

When `next_cursor` comes back `null`, you've reached the end.

### Properties

* **Opaque.** Treat cursors as opaque strings. The format is HMAC-signed and
  bound to your tenant; tampering, forging, or replaying a cursor against a
  different API key all return `400 Bad Request`.
* **Stable under mid-walk writes.** Inserts and deletes that happen between
  requests do not cause duplicates or skips on the cursor path. New rows
  created with a `created_at` newer than the cursor anchor are not surfaced
  in subsequent pages – start a new walk to pick them up.
* **Forward only.** Reverse traversal is not supported; restart the walk
  from the beginning to revisit earlier rows.
* **Deterministic ordering.** Cursor mode returns rows ordered by
  `created_at DESC, id DESC`. The legacy `sort_by` / `sort_order` params on
  `GET /developer/vouchers` are ignored when `cursor` or `limit` is set.
* **Per-endpoint limit caps:** vouchers 100, customers 100, products 100.

## Page pagination (deprecated)

<Warning>
  The `page` / `per_page` shape is kept for backward compatibility but is
  subject to row drift if the underlying data mutates mid-walk (a row
  inserted at the start can shift later pages, causing duplicates or skips).
  New integrations should use cursor pagination.
</Warning>

Pass `page` and `per_page`. The response includes `total`, `page`, and
`per_page`.

| Parameter  | Type    | Default | Min | Max   | Notes                 |
| ---------- | ------- | ------- | --- | ----- | --------------------- |
| `page`     | integer | `1`     | `1` | –     | 1-indexed page number |
| `per_page` | integer | varies  | `1` | `100` | Page size cap         |

The default `per_page` differs by endpoint:

* `GET /developer/vouchers` – default 50, max 100
* `GET /developer/customers` – default 50, max 100
* `GET /developer/products` – default 20, max 100

```json theme={null}
{
  "vouchers": [/* …items… */],
  "total": 247,
  "page": 1,
  "per_page": 50
}
```

Compute "has more":

```ts theme={null}
const lastPage = Math.ceil(response.total / response.per_page);
const hasMore = response.page < lastPage;
```

<Note>
  When the customer search uses a non-email term, the server caps the
  underlying lookup at 1,000 candidate rows for memory safety. Very large
  tenants searching for a common substring may not see every match – narrow
  the search term to recover precision. Both cursor and page shapes apply
  the same 1,000-row cap, so they return identical row sets for the same
  search query.
</Note>
