Pagination, errors, idempotency
Three cross-cutting mechanisms, identical across the API.
Cursor pagination
Collections added in 3.0 (events, webhook deliveries, time entries, fiscal journal) are read by cursor:
GET /companies/{id}/events?limit=50
→ { "data": [ … ], "nextCursor": "MTIzNDU", "hasMore": true }
GET /companies/{id}/events?limit=50&cursor=MTIzNDU
The cursor is opaque: do not decode or build it. It stays valid whatever is inserted between two pages. limit is 50 by default and 200 at most.
Older collections (sales, purchases, partners, transactions) return an array, filtered by period and capped by limit (100 by default):
GET /companies/{id}/sales?from=2026-09-01&to=2026-09-30&limit=200
Sales and purchases also take page and size for classic pagination: ?page=2&size=50 then returns { data, total, totals } — the rows, their count, and the period's sums. For a full sync, walk month by month or page by page.
Errors
An error is a 4xx or 5xx response whose body carries a stable code:
{ "statusCode": 400, "message": "field-required:walletId", "error": "Bad Request" }
The code is meant for a program: a lowercase identifier, sometimes followed by a detail after :. The app turns it into a sentence; so can you. The most common:
| Status | Code | Meaning |
|---|---|---|
| 400 | field-required:<field> |
A required field is missing |
| 400 | amount-required |
The amount must be above zero |
| 400 | amount-above-outstanding |
The payment exceeds the balance due |
| 400 | insufficient-stock:<item>:<available> |
Not enough in stock |
| 400 | plan-limit:<key>:<allowed> |
The plan's ceiling is reached |
| 401 | api-key-invalid |
Unknown or revoked key |
| 403 | forbidden |
Missing scope, or a write with a test key |
| 403 | missing-permission |
The scope does not cover this action |
| 404 | company-not-found, sale-not-found, … |
The resource does not exist for this company |
| 409 | in-flight |
The same request, with the same idempotency key, is still running |
| 429 | too-many-requests |
Too many requests for this key; see Retry-After |
A 5xx is our fault. Retry with a growing delay; the request is safe to replay if it carried an idempotency key.
Idempotency
Every write may carry an Idempotency-Key header:
POST /companies/{id}/sales
Idempotency-Key: till-7-2026-09-28-00042
The first request to arrive with a given key is executed; its response (status included) is kept 24 hours and replayed to any request presenting the same key, with the header Idempotent-Replay: true. A request arriving while the first is still running gets 409.
The key is free text, 8 to 200 characters. Pick something that identifies the business operation (the till's receipt number, the row id in your system), not a UUID drawn at send time — otherwise a retry draws another one and the mechanism does nothing.
Dates and filters
fromandtotake a date (2026-09-01) or an ISO instant.tois inclusive up to the end of the day in the company's time zone.- Filters are query parameters (
?status=paid&businessUnitId=…). Filterable fields are listed in the OpenAPI schema.
Useful headers
| Header | Meaning |
|---|---|
X-Request-Id (response) |
The request's id, to quote in a report |
Retry-After (429 response) |
Seconds before retrying |
Idempotent-Replay: true (response) |
The response comes from the idempotency cache |
Accept-Language (request) |
The language of generated wording (PDFs, reminders) |