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, 1100. 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.

Something inaccurate or missing? support@billey.nl