Conventions & errors
Headers
Section titled “Headers”| Header | When | Notes |
|---|---|---|
Authorization: Bearer <jwt> |
Session routes | Staff or customer session. See Authentication. |
x-api-key |
Key routes | Publishable, operator or content key. |
x-tenant-id |
With a publishable or operator key | UUID of the tenant the key belongs to. Not used with content keys (the merchant slug is in the path) or sessions (the tenant is in the session). |
x-idempotency-key |
Every mutation; required on money-moving routes | Any string unique to the attempt. Retrying with the same key returns the original outcome. Missing on a route that requires it → 400 idempotency_key_required. |
x-correlation-id |
Optional | Carried through logs and outbox events; generated when absent. |
If-Match |
Content revisions | The sequence you last read; 428 without it, 409 with currentSequence on a lost race. |
content-type: application/json |
Any request with a body |
Idempotency
Section titled “Idempotency”Every write is a command with an idempotency key. The key is scoped to the tenant, so two tenants may reuse the same string without colliding. Use a fresh UUID per attempt and reuse it for retries of that attempt: a timed-out purchase retried with the same key returns the original charge rather than making a second one; a cancellation retried with the same key is a no-op.
Routes that move money — purchases, captures, cancellations, rebooks, balance charges, money-bearing amendments — refuse to run without the header.
Errors
Section titled “Errors”Failures return a uniform envelope with a stable, machine-readable reason:
{ "reason": "sold_out" }Match on reason, not on the HTTP status or any text. Reasons are deliberately terse and do not explain themselves (except where explaining is safe and useful, such as session_expired).
| Status | Typical reasons | What it means |
|---|---|---|
400 |
invalid_json, idempotency_key_required, invalid_rental_days |
The request is malformed. Fix the client. |
401 |
unauthenticated, invalid_key, invalid_session, session_expired |
No usable credential. |
402 |
processor_not_configured |
The tenant has not connected a payment processor. Nothing was charged. |
403 |
— | The credential is valid but lacks the scope or permission. |
404 |
— | Not found, or not yours: cross-tenant rows, consumed tokens and unknown ids are indistinguishable by design. |
409 |
sold_out, capacity_full, stale_version |
A conflict with current state. Re-read and decide; nothing was changed. |
422 |
rail_refused, slot_closed, validation reasons |
The request was understood and refused by a business rule or the processor. |
428 |
— | A required precondition header (If-Match) is missing. |
429 |
— | Rate limited (login and refresh). |
502 |
— | The processor could not be reached with the stored credential. |
503 |
store_unavailable, *_unavailable |
A backend is not wired or not reachable. The server fails closed: a write that persisted nothing never returns a silent 200. Retry later. |
Some conflicts carry extra fields beside reason — a stale amendment returns stored_version, a lost content race returns currentSequence, a capacity refusal may name the pool_key — so parse the body as an object, not a string.
Identifiers
Section titled “Identifiers”Ids are UUIDs. A few are minted by the client: the participant_id on a booking-intent participant and the obligation_ref on a purchase. Minting them client-side lets you record the id before the call succeeds, which is what makes a retry safe.
A booking has two handles: booking_id (the record) and obligation_ref (its money). Lifecycle calls on the booking take one or both; the reference for each operation says which.
Amounts are integers in the currency’s minor unit, named *_minor (net_minor, tax_minor, gross_minor). There are no floats. No request carries a price; where a request affects money it carries facts and the server prices them.
Dates and times
Section titled “Dates and times”- Instants are RFC 3339 strings in UTC (
2026-11-05T14:00:00Z) or Unix epoch milliseconds in fields named*_ms. - Civil dates for accounting (the effective date of a cancellation, rebook or purchase) are sent as separate
effective_year,effective_month,effective_dayintegers or as a{year, month, day}object, never as a timestamp, so a period close is not at the mercy of a timezone. - Departure start times are UTC instants; render them in the tenant’s local time.
Pagination
Section titled “Pagination”List endpoints that page return next_cursor (opaque; null on the last page). Pass it back as ?cursor= to continue. Content list endpoints use the Payload-style envelope instead (docs, totalDocs, page, hasNextPage).
Open enums
Section titled “Open enums”Response-side string fields that can grow (statuses, kinds, reasons) are documented with their known values but are not closed: decode them as strings and fall back gracefully on a value you do not know. Adding a value is not a breaking change. Request-side enums are strict and validated.
Versioning
Section titled “Versioning”The API is additive. New fields and new values on open enums arrive without notice; fields are not removed or renamed without a published migration. The reference’s version is its generation date.