Pagination
Every list endpoint (GET /customers, GET /invoices, …) returns a
cursor-paginated envelope:
{
"object": "list",
"data": [ … ],
"has_more": true,
"next_cursor": "eyJpZCI6…"
}
Reading a full list
Loop until has_more is false:
let cursor = null
const all = []
do {
const url = new URL('https://www.tabba.io/api/v1/customers')
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)
Cursors are opaque and stable across mutations — a new item inserted mid-iteration will not throw off your loop or cause duplicates.
Incremental sync with updated_since
Pass updated_since=<ISO timestamp> to fetch only what changed after
a given point:
curl "https://www.tabba.io/api/v1/customers?updated_since=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer $TABBA_API_KEY"
Persist the maximum updated_at you observed in each page and pass
it back on the next poll. Combine with cursor if a page-size
crossing is possible: updated_since narrows the working set,
cursor walks it.
Common filters
| Filter | Endpoints | Meaning |
|---|---|---|
external_id | most | Exact match on your ext id |
customer | invoices, recognition | Filter by customer ref |
status | invoices, credit notes | e.g. issued, draft |
updated_since | all lists | ISO 8601 timestamp |
Filters and cursor may be combined freely.
Page size
Default page size is 25; the maximum is 100. Pass limit=<n>:
curl "https://www.tabba.io/api/v1/customers?limit=100" \
-H "Authorization: Bearer $TABBA_API_KEY"