# Pagination

List endpoints return their rows in pages. Page with the `cursor` returned by
the previous response: each page then costs the same however far into the list
it is, and a row added while you page does not make another row appear twice or
go missing. Paging by `page` number still works, but is deprecated. From
2026-12-31, a list with more than 10,000 matching rows can only be paged past
its first page by cursor, so sort it by a column that pages by cursor (see
[Lists that page by cursor](#lists-that-page-by-cursor)) or narrow the filters
below the limit.

## Paging with a cursor

Request the first page without a cursor. Each response carries `nextCursor`,
the cursor for the page after it, and `hasMore`. Pass `nextCursor` back as
`cursor`, with the same filters and sort, until `nextCursor` is `null`.

```bash
curl 'https://api.ledgerbee.com/api/v1/journal-entries?limit=100&fromDate=2026-01-01' \
  -H 'x-api-key: <your-key>'
```

```json
{
  "data": [{ "id": "01a117e5-adfe-7637-ab7c-0023d4f58531" }],
  "total": 105705,
  "page": 1,
  "limit": 100,
  "totalPages": 1058,
  "nextCursor": "eyJ2IjoxLCJmIjoiOWMxZjQyYWIzMTA4ZDc2ZSIsImsiOlsiMjAyNi0wMS0yMCJdfQ",
  "hasMore": true
}
```

```bash
curl 'https://api.ledgerbee.com/api/v1/journal-entries?limit=100&fromDate=2026-01-01&cursor=eyJ2IjoxLCJmIjoiOWMxZjQyYWIzMTA4ZDc2ZSIsImsiOlsiMjAyNi0wMS0yMCJdfQ' \
  -H 'x-api-key: <your-key>'
```

A cursor is opaque: pass it back unchanged and do not build one yourself. It is
bound to the filters and sort of the request that returned it, so a request that
changes either is refused. `limit` may change between pages. Do not send `page`
together with `cursor`.

A row added during the walk is returned only if it sorts after the current
cursor. `total` counts every row that matches the filters and can lag behind
rows being added. Stop when `nextCursor` is `null`, not when you have read
`total` rows.

## Lists that page by cursor

| Endpoint | Sorts that page by cursor |
| --- | --- |
| `GET /api/v1/journal-entries` | `date`, `createdAt` (both `ASC` and `DESC`) |
| `GET /api/v2/draft-journal-entries` | the list's fixed order |
| `GET /api/v1/invoices` | the list's fixed order |
| `GET /api/v1/credit-notes` | the list's fixed order |
| `GET /api/v1/quotes` | `createdAt`, `date`, `amount`, `status` |
| `GET /api/v1/order-confirmations` | `createdAt`, `date`, `amount`, `status` |
| `GET /api/v1/customers` | the list's fixed order |
| `GET /api/v1/suppliers` | the list's fixed order |
| `GET /api/v1/vendors` (deprecated alias of suppliers) | the list's fixed order |
| `GET /api/v1/advisor/clients` | the list's fixed order; the cursor is also bound to `updatedAtFrom` |
| `GET /api/v1/products` | `productNumber`, `name`, `createdAt` |
| `GET /api/v1/subscriptions` | the list's fixed order |
| `GET /api/v1/shipments` | the list's fixed order |
| `GET /api/v1/stock/on-hand` | the list's fixed order |
| `GET /api/v1/stock/movements` | the list's fixed order |

A sort on a column that can be empty, such as `documentDate` on journal
entries or `documentNumber` and `validUntil` on quotes, cannot page by cursor:
its responses carry `nextCursor: null` even when `hasMore` is `true`, and a
request combining it with `cursor` is refused. Page such a sort by `page`
number while `hasMore` is `true`; from 2026-12-31 that works only while the list
has at most 10,000 matching rows, so narrow the filters or choose a sort from the
table above for a larger one. Each endpoint's parameters and defaults are in
the [API Reference](/api).

## Paging by page number (deprecated)

`page` and `limit` page by position: page `n` skips the first `(n - 1) × limit`
matching rows. Every request past the first page reads the rows before it
again, so walking a large list this way slows with every page, and rows added
during the walk shift the pages under you. A page past the last one returns an
empty `data` array.

Paging by page number is deprecated and stays supported on lists of up to
10,000 matching rows. On a larger list, from 2026-12-31, a page past the first
is refused with `400 PAGINATION_OFFSET_TOO_DEEP`. Page such a list by cursor
with a sort that supports it, or narrow the filters (for example a `fromDate`
window) to keep it under the limit.

A response to a request for a page past the first carries these headers:

| Header | Value | Sent | Meaning |
| --- | --- | --- | --- |
| `Deprecation` | `@1791417600` | always | Paging by page number was deprecated on 2026-10-08 ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745), Unix seconds). |
| `Link` | `<https://docs.ledgerbee.com/guides/pagination>; rel="deprecation"` | always | This page. |
| `Sunset` | `Thu, 31 Dec 2026 00:00:00 GMT` | lists with more than 10,000 matching rows | The date the request starts being refused ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)). |

## Errors

| Condition | What you observe | Fix |
| --- | --- | --- |
| The cursor was not returned by this endpoint, or the filters or sort changed since | `400 PAGINATION_INVALID_CURSOR` | Restart from the first page with the new filters. |
| `cursor` and `page` sent together | `400 PAGINATION_CURSOR_WITH_PAGE` | Drop `page`. |
| `cursor` sent with a sort that cannot page by cursor | `400 PAGINATION_CURSOR_SORT_NOT_SUPPORTED` | Sort by a column listed in the table above. |
| A sort the endpoint lists but does not support | `400 PAGINATION_SORT_NOT_SUPPORTED` | Choose another `sortBy`. |
| From 2026-12-31: a page past the first, by page number, on a list with more than 10,000 matching rows | `400 PAGINATION_OFFSET_TOO_DEEP` | Page with `cursor`, or narrow the filters. |
Error bodies follow the shape described in [Errors](/guides/errors).
