# Pagination

> Cursors, the order of a list, and following a list with updated_since.

Source: https://www.tabba.io/developers/api/pagination

Every list endpoint (`GET /customers`, `GET /invoices`, and the others)
answers one page in the same envelope:

```json
{
  "object": "list",
  "data": [ … ],
  "has_more": true,
  "next_cursor": "eyJzIjoi…"
}
```

`next_cursor` is `null` on the last page.

## Reading a whole list [#reading-a-whole-list]

Pass the `next_cursor` of one page as the `cursor` of the next, until
`has_more` is `false`:

```js
let cursor = null
const all = []
do {
  const url = new URL('https://www.tabba.io/api/v1/customers')
  url.searchParams.set('limit', '100')
  if (cursor) url.searchParams.set('cursor', cursor)
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.TABBA_API_KEY}` },
  })
  const page = await res.json()
  all.push(...page.data)
  cursor = page.has_more ? page.next_cursor : null
} while (cursor)
```

A cursor is opaque: pass it back as you were given it. A value that is not a
cursor of the API is refused with `VALIDATION_FAILED` (`422`).

## Order [#order]

A list is ordered by when a record last changed, **oldest change first**,
then by id. The cursor names the record a page ended on, so the next page is
everything after it in that order.

A record that changes while you walk the list moves to the end of it and is
met again. None is skipped. Write your reader so that meeting a record twice
is harmless: key it by `id`, and let the later copy win.

The occurrences of a recurring series are the exception: they are listed in
the order of their periods.

## Page size [#page-size]

A page holds 25 records unless you say otherwise, and at most 100. Pass
`limit`:

```bash
curl "https://www.tabba.io/api/v1/customers?limit=100" \
  -H "Authorization: Bearer $TABBA_API_KEY"
```

## Following a list with updated\_since [#following-a-list-with-updated_since]

`updated_since` narrows a list to the records changed **at or after** an
instant. It is an ISO 8601 timestamp with an offset, such as
`2026-07-01T00:00:00Z`.

```bash
curl "https://www.tabba.io/api/v1/customers?updated_since=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer $TABBA_API_KEY"
```

How to follow a list depends on whose record it is.

**Customers, products and people.** Remember the newest `updated_at` your
last walk was answered, and ask from it. That record comes again, with
everything changed after it. For customers and products this misses nothing
and needs no overlap: a change is stamped as it is written, in the order
changes become visible. The instant is one Tabba handed out, so your own
clock is never held against Tabba's.

**Invoices, recurring series and credit notes.** The change stamp is taken
when the record is written. Follow the list from the instant your last walk
started. No overlap is needed.

Three things do not move a change stamp:

* A change of a customer's or a product's `external_id` alone.
* A change to a line of a recurring series alone. The series' own
  `updated_at` stays where it was.
* An import from the accounting system that found nothing changed.

A customer or a product that was switched off or deleted in the accounting
system is no longer listed, so a followed list does not tell you that it
left. The `customer.updated` and `product.updated` events do: see
[Webhooks](/developers/api/webhooks).

## Filters [#filters]

| Filter          | Lists                                                  | Meaning                                                                        |
| --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `external_id`   | users, customers, products, invoices, recurring series | Only the record that carries this external id                                  |
| `customer`      | invoices, recurring series                             | Only records of this customer: its `id`, or `ext:` and its external id         |
| `status`        | invoices, recurring series                             | `draft` or `issued` for an invoice, `draft` or `active` for a series           |
| `updated_since` | every list but occurrences                             | Only records changed at or after the instant                                   |
| `from`, `to`    | occurrences                                            | Only occurrences whose period starts on or after `from`, and on or before `to` |

Filters and `cursor` combine. Keep the filters the same from page to page of
one walk.

`GET /invoices` lists every invoice of the company, the occurrences of
recurring series among them. An occurrence states its `series_id`.

`GET /invoices/{ref}/credit-notes` lists only booked credit notes.

## Occurrences have no last page [#occurrences-have-no-last-page]

`GET /recurring-series/{ref}/occurrences` lists the occurrences of a series
from its start, or from `from`, to its end date, or to `to`. A series with no
end date has no last occurrence, so `has_more` never turns `false` on it.
Bound the walk with `to`, or stop when you have what you need. Reading an
occurrence never makes it an invoice.

## Rate limits [#rate-limits]

A walk spends one request per page against the key's limit. Ask for 100
records a page when you read a whole list. The limits and their headers are
on the [Errors](/developers/api/errors) page.
