SeatBuilderSeatBuilder Docs

Error model

The exact runtime envelope every SeatBuilder error response carries, the stable machine-readable code table, and what each HTTP status means.

Error model

Every error response from the SeatBuilder public API follows the same JSON envelope:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "code": "validation_failed",
  "failed": [ /* only on seat hold/book/release 409s — see below */ ],
  "errors": { /* only on 400 validation failures — see below */ }
}
  • statusCode — the HTTP status code; mirrors the response status.
  • error — the HTTP reason phrase for statusCode (e.g. "Bad Request", "Not Found", "Conflict").
  • message — a stable, human-readable summary. Safe to surface verbatim to end users, but do not branch your integration logic on its exact wording — the copy can change between releases.
  • code — a stable, snake_case machine-readable code. Branch on code, not message. message wording may change between releases; code will not.
  • failed — present only on the all-or-nothing 409s from /seats/hold, /seats/book, and /seats/release: the specific objectLabels that blocked the batch, each with a reason.
  • errors — present only on 400 validation_failed: a dot-path map of field-level Zod messages.

Error codes

codeTypical statusCodeMeaning
validation_failed400The request body failed schema validation. See errors.
unauthorized401Missing or invalid X-Api-Key.
forbidden403Cross-workspace or cross-environment access denied.
public_key_not_allowed403A publishable (pk_*) key called an endpoint that requires a secret key — book, extend, or reset.
not_found404A generic resource (chart, webhook, …) doesn't exist in the authenticated workspace/environment.
event_not_found404The eventKey in the URL doesn't exist in the authenticated workspace/environment.
event_key_conflict409POST /events with an eventKey already used by a different chart, environment or workspace. Retrying with the same eventKey + chartKey is not an error: it returns the existing event with 200.
event_has_reservations409DELETE /events/{eventKey} on an event that still has booked or live-held seats. Release them first.
seat_unavailable409A seat in a /hold request is already held or booked by another token.
not_held_by_token409A label in a /book or /release request is not (live-)held by the caller's holdToken — it belongs to another token, the token never held it (this includes booking a general-admission area without a GA hold), or (for /release) it's booked and the caller used a public key.
hold_expired410 (book) / included in extend failed[]The token's hold on this label (seat or GA area) has expired. On /book this is a 410 and nothing is booked; on /extend it's a failed[] reason, not a request failure. Reported after the expiry sweep too: an expired hold is remembered for its token until the seat is held again (by any token) or an operator force-releases or changes it; for a GA area the record stays for that token.
hold_token_required400/release was called without holdToken from a public key, without holdToken or objectLabels at all, or against a general-admission area with no holdToken.
ga_label_requires_quantity400A single-element objectLabels[] names a GA area but omits objectType: "generalAdmission" / quantity.
ga_capacity_exceeded409A GA hold (new or resized) doesn't fit the area's remaining capacity.
no_active_hold409/extend was called with a holdToken that has no hold (live or expired) or booking at all on this event.
publish_affects_reservations409POST /charts/{chartKey}/publish would remove, re-type, re-categorise or over-shrink a booked or held label on an event of this chart. failed[] lists each one with eventKey, environment, reason, booked, held. Retry with ?force=true to publish anyway.
rate_limited429Too many requests from your IP address. See Rate limits.
plan_limit_reached402The workspace's plan limit was hit (e.g. chart or event count).
sandbox_only403POST /events/{eventKey}/reset was called against a production event.
conflict409A generic concurrency conflict — safe to retry the request as-is.
bad_request400A generic malformed request with no more specific code.
internal_error500An unhandled exception inside the platform.

Validation errors (400, validation_failed)

Zod validation runs before any controller handler. When a request body violates a schema, the server returns:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Validation failed",
  "code": "validation_failed",
  "errors": {
    "name": ["String must contain at least 3 character(s)"],
    "draftVersion": ["Expected object, received string"]
  }
}

The errors map has the following shape:

  • Keys are dot-paths into the request body — e.g., name, draftVersion, sections.0.rows.2.label. The synthetic key _root is used for whole-body violations (e.g., the request body itself is the wrong type).
  • Values are arrays of human-readable Zod messages. A single field can fail multiple constraints (e.g., min length AND format) and each failure is reported as a separate string.

Display these messages directly under the offending form field; the server has already formatted them for UI consumption.

Authentication errors (401, unauthorized)

Returned when the X-Api-Key header is missing, malformed, or hashes to a key the server doesn't recognise.

{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Missing API key",
  "code": "unauthorized"
}

The same envelope is returned for the "invalid key" case ("message": "Invalid API key") — the server does not distinguish unknown keys from forged keys to keep enumeration noisy.

Authorization errors (403)

Returned when the key authenticates but is not allowed to perform the requested action. Two distinct codes cover this:

  • forbidden — wrong workspace or wrong environment.
  • public_key_not_allowed — a publishable (pk_*) key called book, extend, or reset, all of which are secret-key-only actions.
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "This endpoint does not accept public keys",
  "code": "public_key_not_allowed"
}

Not found (404, not_found / event_not_found)

Returned when the requested chart, event, webhook, or seat does not exist inside the authenticated workspace and environment.

{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Event not found",
  "code": "event_not_found"
}

A 404 never leaks data — if a resource exists in a different workspace or environment but matches the requested key, the response is the same 404.

Conflict (409)

The most common family of write-endpoint errors. Every 409 from hold, book, release, or extend is safe to inspect and react to programmatically:

  • seat_unavailable (/hold) — one or more requested seats are already held or booked by a different token. Nothing was held (all-or-nothing); failed[] lists the blocking labels.
  • not_held_by_token (/book, /release) — one or more labels are not live-held by the caller's token (someone else holds it, it's booked, or — for /release from a public key — it's booked at all). Nothing was booked or released; failed[] lists the labels.
  • ga_capacity_exceeded (/hold) — a GA hold (fresh or a same-token resize) doesn't fit the area's remaining capacity.
  • no_active_hold (/extend) — the token has no hold or booking at all on this event.
  • conflict — a generic concurrency conflict (e.g. a GA pool changed mid-request); safe to retry.
{
  "statusCode": 409,
  "error": "Conflict",
  "message": "One or more seats could not be released",
  "code": "not_held_by_token",
  "failed": [{ "objectLabel": "A-13", "reason": "not_held_by_token" }]
}

Retry only after re-prompting the buyer (or, for /extend, dropping the failed labels) — a blind retry of the same request will not succeed.

Rate limits (429)

Limits apply per client IP address, before the request reaches the API:

EndpointsSustained rateBurst
/api/v1/events/{eventKey}/seats/*, /api/v1/events/{eventKey}/status20 requests/s40
/api/v1/auth/*5 requests/s5
All other /api/v1/* endpoints, including POST /events and DELETE /events/{eventKey}30 requests/s60

A request over the limit is rejected immediately with HTTP 429, a Retry-After: 1 header and the usual error body:

{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many requests", "code": "rate_limited" }

Wait at least Retry-After seconds, backing off further on repeats (for example 1 s, 2 s, 4 s), and retry the same request. For bulk jobs such as creating hundreds of events, stay under about 10 requests/s with at most 5 in flight, and pass your own eventKey so retries are safe.

Hold expired (410, hold_expired)

Returned only from /book, when a label (seat or GA area) was held by the caller's token but that hold's TTL has already elapsed. The hold is already free for anyone else to take; nothing from the request was booked.

This holds both right after expiry and after the background expiry sweep has run. An expired hold is remembered for its token until the seat is held again (by any token) or an operator force-releases or changes it. For a GA area the record stays for that token. Releasing an expired hold does not clear it (it is already free, so /release reports it under alreadyFree). Once another token holds the seat, your token gets 409 not_held_by_token for it instead (someone else has it now).

{
  "statusCode": 410,
  "error": "Gone",
  "message": "One or more holds have expired",
  "code": "hold_expired",
  "failed": [{ "objectLabel": "A-12", "reason": "hold_expired" }]
}

/extend reports the same underlying condition differently: an expired label comes back in the 200 response's failed[] array with reason: "hold_expired" rather than failing the whole call — extend is partial-success, not all-or-nothing.

Clear the buyer's client-side selection and prompt them to re-pick seats. See Hold seats and confirm at checkout for the full recovery flow.

Server errors (500, internal_error)

Returned for unhandled exceptions inside the platform. Retry with exponential backoff; if the error persists, capture the request ID (X-Request-Id response header) and contact support.

{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An unexpected error occurred",
  "code": "internal_error"
}
Error model — SeatBuilder Docs