Pagination
Cursors, the order of a list, and following a list with updated_since.
Every list endpoint (GET /customers, GET /invoices, and the others)
answers one page in the same envelope:
{
"object": "list",
"data": [ … ],
"has_more": true,
"next_cursor": "eyJzIjoi…"
}next_cursor is null on the last page.
Reading a whole list
Pass the next_cursor of one page as the cursor of the next, until
has_more is false:
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
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
A page holds 25 records unless you say otherwise, and at most 100. Pass
limit:
curl "https://www.tabba.io/api/v1/customers?limit=100" \
-H "Authorization: Bearer $TABBA_API_KEY"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.
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_idalone. - A change to a line of a recurring series alone. The series' own
updated_atstays 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.
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
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
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 page.