Guides
Pagination and idempotency
Cursor pagination, change feeds with updated_since, and Idempotency-Key headers for safe retries.
Two conventions apply to every /public/v1 operation: lists are paginated with a cursor, and every POST carries an idempotency key so it can be retried safely.
Pagination
Every list operation (GET /public/v1/members, GET /public/v1/vouchers, ...) returns a page:
{
"data": [ { "id": "c41f…", "updated_at": "2026-10-24T02:15:09.123Z" } ],
"next_cursor": "WyIyMDI2LTEwLTI0VDAyOjE1OjA5LjEyMyswMDowMCIsImM0MWY…",
"has_more": true
}| Query parameter | Meaning |
|---|---|
limit |
Page size, 1 to 200. Default 50. |
cursor |
The next_cursor of the previous page. Opaque: pass it back unchanged. |
updated_since |
ISO 8601 timestamp with an offset. Only records updated at or after it. |
- Keep requesting with
cursoruntilhas_moreisfalse(next_cursoris thennull). - Pagination is keyset-based, not offset-based. A cursor stays valid while records are added or changed, so no record is skipped or repeated within a pass.
- A cursor that did not come from
next_cursoris rejected with400 INVALID_CURSOR. Keep the other query parameters the same while you follow a cursor.
Syncing changes with updated_since
Lists are ordered by updated_at, then id. Together with updated_since that makes every list a change feed:
- First sync: page through the list without
updated_since. - Remember the largest
updated_atyou received. - Next sync: page through with
updated_sinceset to that value.
updated_since is inclusive, so the last record of one sync comes back at the start of the next. Upsert by id and the repeat does no harm.
curl -sS -G "$API/public/v1/members" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "updated_since=2026-10-24T00:00:00Z" \
--data-urlencode "limit=200"Idempotency keys
Networks fail: a request can succeed at Clive while its response is lost on the way back. Idempotency keys let you retry without doing the work twice (two members, two redemptions).
- Every
POSTmust carry anIdempotency-Keyheader. Without one the API answers400 IDEMPOTENCY_KEY_REQUIRED. PATCHandDELETEaccept the header too, and it is worth sending.- Use a new unique value for each operation, such as a UUID: 1 to 255 printable ASCII characters. Reuse it only when you retry that same operation.
curl -sS -X POST "$API/public/v1/vouchers/redeem" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: 6f1c2a9e-3b7d-4e4a-9c1f-0d2b8e5a7c31" \
-H "Content-Type: application/json" \
--data '{ "code": "AAMX7K2P9", "venue_id": "5a1d9e2b-3c4f-4a6b-8d7e-9f0a1b2c3d4e" }'What happens when a key is used again:
| Situation | Response |
|---|---|
| Same key, same request, first one finished | The stored response is sent again, with the header Idempotent-Replayed: true. Nothing runs twice. |
| Same key, same request, first one still running | 409 IDEMPOTENCY_IN_PROGRESS with Retry-After. Retry after that many seconds. |
| Same key, different request | 422 IDEMPOTENCY_KEY_REUSED. This is a bug in the client: use a new key for a new operation. |
| The key is more than 24 hours old | It has expired and the request runs as new. |
"Same request" means the same method, path, query string and JSON body (the order of keys in the body doesn't matter). Responses are stored unless they are server errors (5xx), so a 5xx can be retried with the same key and really runs again. A request refused with 429 never ran, so retrying it with the same key is always safe.
Retrying
Retry on network errors, timeouts, 429 and 5xx, with exponential back-off, and always with the same idempotency key. Don't retry other 4xx responses unchanged: fix the request first.