---
title: "API errors"
description: "The SeatLayer API error body, every public error code grouped by area with its HTTP status, and which errors are safe to retry."
---

Every error from the SeatLayer API has the same shape: a JSON object whose
first key, `error`, is a stable machine code. Branch on `error` (and `code`
when present), never on `message`.

## The error body

```json title="404 response"
{
  "error": "not_found"
}
```

```json title="409 response with detail"
{
  "error": "order_edit_in_progress",
  "code": "order_edit_in_progress",
  "retryable": true,
  "conflicts": [{ "label": "A-12", "status": "blocked" }]
}
```

| Field | Always present | Meaning |
|---|---|---|
| `error` | Yes | Stable machine code, never a sentence |
| `code` | No | A more specific reason when `error` is a broad class, such as `error: "forbidden"` with `code: "missing_manage_capability"`. Often equal to `error` |
| `message` | No | Human-readable text for logs and operators. It can change; do not parse it |
| `retryable` | No | `true` when the same request can succeed later without changes |
| `retryAfterSeconds` | No | Seconds to wait, on rate limits and maintenance |
| `requestId` | No | Sent in the body on `500` and `503`. Every response also carries an `X-Request-ID` header |

Routes add their own detail fields next to these, for example `conflicts`,
`issues`, `maxBytes`, or `currentUpdatedAt`. Treat unknown fields as extra
information, not as a different error.

The [OpenAPI document](/openapi.json) lists the exact error codes each
operation can return. This page groups them so you can write one handler.

## Status codes

| Status | Meaning | Retry |
|---|---|---|
| `400` | The request is missing a required field or is malformed | No. Fix the request |
| `401` | No valid credential | No. Check the key |
| `402` | Not enough credits to create inventory | No. Buy credits first |
| `403` | The credential cannot do this, or the resource is in the other mode | No |
| `404` | The resource does not exist for this account and mode | No |
| `409` | The current state does not allow the change | Only when `retryable: true` |
| `413` | Body or upload too large | No |
| `415` | Unsupported file type | No |
| `422` | The request is well formed but a value is invalid | No |
| `429` | [Rate limited](/server-api/rate-limits/) | Yes, after `Retry-After` |
| `500` | Unexpected server error | Reads: yes. Writes: see below |
| `502` | A payment provider step needs a retry | Yes, with the same `Idempotency-Key` |
| `503` | Temporarily unavailable or in maintenance | Yes, after `Retry-After` |

## Codes by area

### Authentication and access

| Status | `error` / `code` | When |
|---|---|---|
| `401` | `unauthorized` / `bad_key` | Missing, revoked, or malformed `sk_…` secret key |
| `401` | `unauthorized` / `bad_manage_session`, `manage_session_expired` | Event manage token is invalid or expired |
| `403` | `mode_mismatch` | A test key used on a live resource, or the reverse |
| `403` | `forbidden` / `missing_manage_capability` | The manage token lacks the capability, such as `event:block` |
| `403` | `forbidden` / `event_scope_mismatch` | The manage token belongs to another event |
| `403` | `forbidden` | The dashboard role cannot make this change |
| `403` | `channel_access_denied` | The seats belong to a sales channel this caller cannot use |
| `403` | `module_not_enabled` | The feature is not turned on for your account, for example Seasons or Event Configurations |

### Validation

| Status | Examples |
|---|---|
| `400` | `name_required`, `chartId_required`, `selections_required`, `holdId_required`, `qty_required`, `labels_required`, `labels_and_holdId_required`, `bookingRef_required`, `hold_or_labels_and_bookingRef_required`, `objects_required`, `rules_required`, `releaseAt_required`, `blockRef_required`, `invalid_zone`, `invalid_cursor`, `empty_body`, `nothing_to_update` |
| `413` | `chart_too_large`, `image_too_large`, `payload_too_large` |
| `415` | `unsupported_image` |
| `422` | `invalid_request`, `invalid_selection`, `invalid_chart`, `invalid_currency`, `invalid_timezone`, `invalid_listing_id`, `invalid_availability_rule`, `unknown_zone`, `image_dimensions_unsupported` |

### Events and charts

| Status | `error` | When |
|---|---|---|
| `402` | `insufficient_credits` | Creating or copying an event, or adding Season performances, needs more credits |
| `404` | `not_found`, `chart_not_found`, `chart_unavailable` | Unknown event or chart |
| `409` | `event_exists` | The workspace already has an event with this slug |
| `409` | `event_not_mutable`, `invalid_sales_transition`, `sales_revision_conflict` | The event's sales state does not allow the change |
| `409` | `performance_group_locked`, `season_event_locked` | The event belongs to an active Performance Group or Season |
| `409` | `seats_would_vanish` | Updating the chart would remove held or booked seats |
| `409` | `event_configuration_bound` | Unbind the Event Configuration before changing the chart |
| `422` | `chart_not_published`, `invalid_event_inventory`, `inventory_limit_exceeded` | The chart cannot become event inventory |

### Holds, booking, and inventory

| Status | `error` | When |
|---|---|---|
| `404` | `hold_not_found` | The hold id is unknown |
| `409` | `conflict` | One or more seats are not available. `conflicts` lists each label and its status. Nothing in the request was applied |
| `409` | `event_closed` | Sales are closed or outside the sales window |
| `409` | `hold_not_active` | The hold expired or was released before booking |
| `409` | `cannot_extend` | The hold is gone, expired, or at its extension limit |
| `409` | `booking_mismatch` | `unbook` named a `bookingRef` that does not own the seats |
| `409` | `not_booked` | Resale listing named a seat that is not booked |
| `409` | `resale_unsupported_on_seatlayer_checkout` | Resale on a Hosted Ticketing event or ticketed seat |
| `409` | `order_edit_in_progress` | An order edit is changing these seats. Sent with `retryable: true` |
| `409` | `legacy_hold_requires_restart` | The hold predates the current format; start a new hold |
| `422` | `hold_quantity_exceeded` | More seats than the hold limit allows |

### Idempotency

| Status | `error` | When |
|---|---|---|
| `400` | `invalid_idempotency_key` | The key is not 1 to 128 characters of `A-Z a-z 0-9 . _ : -` |
| `400` | `idempotency_not_supported_for_operation` | This operation does not accept `Idempotency-Key` |
| `400` | `idempotency_key_required` | Season order refunds and cancels need a key |
| `409` | `idempotency_conflict` | The key was used before with a different request |
| `409` | `idempotency_in_progress` | The first call with this key is still running. Retry after `Retry-After` |

See [idempotency and conflicts](/server-api/idempotency-and-conflicts/).

### Seasons and Performance Groups

| Status | Examples |
|---|---|
| `409` | `not_a_draft`, `revision_mismatch`, `not_active`, `sales_open`, `sales_not_ended`, `plan_not_published`, `invalid_transition`, `lifecycle_transition_pending`, `amendment_in_flight`, `payment_in_progress`, `book_operation_mismatch`, `booking_not_cancellable`, `hosted_order_use_refund`, `group_not_draft`, `group_not_active`, `member_set_mismatch` |
| `502` | `refund_recovery_required`: retry with the same `Idempotency-Key` |
| `503` | `channel_state_unavailable`, `participant_unavailable`, `recovery_pending`, `lifecycle_unavailable` |

Long Season and Performance Group operations can answer `202 Accepted` with
`Location` and `Retry-After`. That is progress, not an error: poll the
`Location` until the state is terminal.

### Webhooks

| Status | `error` | When |
|---|---|---|
| `403` | `forbidden` | Only an admin can change subscriptions |
| `404` | `not_found` | Unknown subscription |
| `422` | `invalid_url`, `invalid_events`, `invalid_mode`, `invalid_environment` | The endpoint must be public HTTPS and events must be [known names](/webhooks/events/) |

### Service-wide

| Status | `error` | When |
|---|---|---|
| `404` | `not_found` | No such route |
| `429` | `rate_limited` | See [rate limits](/server-api/rate-limits/) |
| `500` | `internal_error` | Unexpected failure. Quote `requestId` to support |
| `503` | `temporarily_unavailable` | A database step timed out. Sent with `retryable: true` and `Retry-After: 1` |
| `503` | `maintenance_mode` | Writes are paused. Reads still work. The body has `retryAfterSeconds`, `resumesAt`, and `message` |

## Which errors to retry

- **Reads (`GET`)**: retry on connection errors, `408`, `429`, and `5xx`, with
  backoff.
- **Writes with `Idempotency-Key`**: retry on the same failures with the
  **same** key. The first result is replayed, so nothing happens twice.
- **Writes without idempotency**: holds, blocks, and most inventory changes.
  Retry `429` and `503` after `Retry-After`, because those did nothing. After a
  timeout or `500`, read the current state before trying again.
- **`409` with `retryable: true`**: wait briefly and send the same request.
- **Every other `4xx`**: do not retry unchanged. Fix the request, pick other
  seats, or show the conflict to the operator.

A booking call keyed by one `bookingRef` is safe to repeat: the same reference
cannot create a second sale. See
[idempotency and conflicts](/server-api/idempotency-and-conflicts/#safe-booking-retries).