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

  • from and to take a date (2026-09-01) or an ISO instant. to is 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)

Something wrong or missing? Write to us.