Pagination
Every list endpoint uses cursor pagination. Cursors are stable under inserts and deletes, which makes them safe for syncing a live dataset — unlike page numbers, a row never shifts between pages while you walk them.
The envelope
A list response wraps the rows in data, with links and meta alongside:
{
"data": [ { "id": 42 }, { "id": 43 } ],
"links": {
"first": null,
"last": null,
"prev": null,
"next": "https://billey.nl/api/v1/invoices?cursor=eyJpZCI6NDN9"
},
"meta": {
"path": "https://billey.nl/api/v1/invoices",
"per_page": 25,
"next_cursor": "eyJpZCI6NDN9",
"prev_cursor": null
}
}
first and last are always null — cursor pagination has no concept of a last page. To walk every page, follow links.next until it is null.
# First page
curl "https://billey.nl/api/v1/invoices?per_page=50" -H "Authorization: Bearer $TOKEN"
# Next page — pass the cursor from links.next / meta.next_cursor
curl "https://billey.nl/api/v1/invoices?per_page=50&cursor=eyJpZCI6NDN9" -H "Authorization: Bearer $TOKEN"
A malformed cursor degrades to the first page rather than erroring, so a corrupted cursor never breaks a sync mid-run.
Query parameters
| Parameter | Description |
|---|---|
per_page |
Rows per page, 1–100. Default 25. |
cursor |
Opaque cursor from links.next / links.prev. |
updated_after |
Return only rows changed after this timestamp — for incremental sync. |
fields |
Comma-separated allowlist of top-level keys to return (sparse fieldsets). |
include_deleted |
On soft-delete list endpoints, set to 1/true to include deleted rows. |
Incremental sync with updated_after
Pass an ISO-8601 timestamp to get only rows whose updated_at is strictly greater than it:
curl "https://billey.nl/api/v1/invoices?updated_after=2026-07-21T10:15:30Z" \
-H "Authorization: Bearer $TOKEN"
The comparison is strict (>), so passing the newest updated_at you already ingested never re-returns that boundary row. Store the highest updated_at you have seen and pass it back on the next sync.
Sparse fieldsets with fields
Ask for only the top-level keys you need:
curl "https://billey.nl/api/v1/clients?fields=id,name,email" \
-H "Authorization: Bearer $TOKEN"
id is always included. Unknown names are ignored, and a blank or absent value returns every field. A nested resource selected this way (for example an invoice's client) is returned in full.
Soft-deleted rows with include_deleted
On endpoints whose resource can be soft-deleted, deleted rows are hidden from the list by default. Pass include_deleted=1 (it also accepts 0/1/true/false) to include them; each carries a non-null deleted_at. Note that a single-resource GET always returns 404 for a soft-deleted row regardless of this flag.