Skip to content

Conventions & errors

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

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.

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.

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.

  • 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_day integers 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.

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).

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.

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.