> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: queue booking failed

> Diagnose a 500 + BOOKING_FAILED on the public queue self-booking POST — tell a transient server-side failure from a just-taken slot, retry once, then hand support a bundle that actually narrows it.

# Troubleshooting: queue booking failed

A caller — or an agent-driven widget — submits the public queue self-booking
form and the `POST /api/v1/public/queues/{tenantId}/{queueId}/appointments`
call comes back `500` with `BOOKING_FAILED`. The code is the route's generic
wrap for a booking attempt that failed below the HTTP layer, so it is a
transient server-side failure until proven otherwise — but it occasionally
hides an already-taken slot that raced availability. This page separates the
two, gets the booking through when a retry is safe, and builds the support
bundle when it is not. Only the `BOOKING_FAILED` 500 lives here: a taken slot
usually rejects cleanly with `409` (`SLOT_ALREADY_BOOKED` /
`SLOT_NOT_AVAILABLE`) and a queue closed to public booking rejects with
`422` (`INVALID_TENANT` or `NO_BOOKABLE_SCHEDULE`) — those codes are working
as designed, not failures.

## Recognize the signal

The booking POST rejects with a 500 and this envelope shape:

```json theme={null}
{
  "error": {
    "code": "BOOKING_FAILED",
    "message": "Couldn't book that appointment. Try again in a moment.",
    "status": 500
  },
  "meta": {
    "request_id": "req_…",
    "timestamp": "2026-09-24T10:15:30.000Z"
  }
}
```

* `error.code` — `BOOKING_FAILED`, one refusal for every server-side failure
  on the booking attempt. It does not name the underlying cause.
* `meta.request_id` — the one handle that ties this response to the server
  log line; keep it for every retry and every escalation.

Reclassify before retrying: if the refusal came back `409` or `422` instead
of a `500`, the slot or the queue gate decided — the booking never reached
the write path, and this page is the wrong runbook.

## Cause table

| Signal                                                                           | Cause                                                                                                                                                                              | Fix                                                                                                                                                                           |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Single `500 BOOKING_FAILED`, then the identical retry succeeds                   | Transient server-side failure on the booking write — the most common shape                                                                                                         | Retry once with the same payload (below); the slot comes back on a normal transient                                                                                           |
| `500 BOOKING_FAILED` again on a slot another caller just grabbed                 | A slot that was still listed when the picker rendered got taken between the availability read and the booking POST, and the conflict surfaced as the 500 wrap instead of the `409` | Re-read `GET /api/v1/public/queues/{tenantId}/{queueId}/availability` and book a slot the fresh read still lists as available                                                 |
| Every slot on the queue returns the same `500` on every attempt                  | Not a transient — the queue's booking path is failing deterministically (unpublished schedule changes, a dispatcher outage, or a data-layer problem on the tenant schema)          | Stop retrying; open a ticket with the bundle below — a retry loop cannot outlast a deterministic failure                                                                      |
| 500 only with a specific payload shape (days in advance, unusual `slot_minutes`) | A payload the availability read accepted but the booking write could not                                                                                                           | Rerun with the defaults (`slot_minutes` 30, `lead_minutes` 60) from a fresh availability read; if defaults book and your shape fails, include both payloads in the escalation |

## Fixes

### 1. Retry once with the same payload

`BOOKING_FAILED` is deliberately retriable — the booking either persisted or
it did not, and a failed attempt never creates a partial slot hold. Send the
identical body once:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/public/queues/{tenantId}/{queueId}/appointments" \
  -H "Content-Type: application/json" \
  -d '{ "slot_start": "2026-09-25T14:00:00Z", "phone": "+15551234567" }'
```

One retry only. Every booking attempt on the public surface counts against
the unauthenticated rate limit the hosted booking page shares with all its
callers — burn the allowance and legitimate callers get refused behind you.

### 2. Poll the availability GET and re-pick if the slot went

When the retry also 500s, re-read the published slots before picking again:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/public/queues/{tenantId}/{queueId}/availability"
```

If your `slot_start` is no longer listed as available, a concurrent caller
took it and the conflict surfaced as the generic 500 — book a slot the fresh
read still shows. If the slot still reads as available and the POST still
500s, the failure is server-side and deterministic; move to escalation.

### 3. Open the ticket with the narrowing bundle

On the second consecutive `BOOKING_FAILED`, stop and page support with:

* the **`meta.request_id`** from the last failing response envelope,
* the **queue id** and the **slot interval** you attempted
  (`slot_start` + `slot_minutes`), and
* the **availability snapshot** — whether the slot still read as available
  after the failure.

The request id is what narrows it: the route logs the underlying failure
server-side per attempt, and support reads that line straight off your
request id instead of re-deriving the cause from the queue config.

## What not to do

* **Do not hammer the endpoint in a retry-loop.** The public booking surface
  sits behind anti-bot token checks and a brutal per-IP rate limit — a tight
  retry loop burns the allowance the hosted booking page shares with every
  other caller, and legitimate bookings start refusing behind you.
* **Do not treat `BOOKING_FAILED` as "the slot is taken."** A taken slot
  gets its own `409` codes; the 500 means the write itself failed. Re-read
  availability before re-picking so you do not abandon a slot that was never
  taken.
* **Do not retry a queue where every slot fails the same way.** Uniform
  failure across every slot is deterministic, not transient — escalate with
  the bundle instead of sampling more slots and tripping the rate limit.
* **Do not book on behalf of a caller through a script.** The booking is the
  caller's consent to be called back at that slot; scripted bulk booking
  stores consent records nobody gave and hits the same anti-bot gate.

## Escalate

If the retry fails and the availability read still lists the slot:

* capture the **`meta.request_id`** from the failing response envelope,
* include the **queue id**, the **slot interval** (`slot_start` and
  `slot_minutes`), and the caller's phone in E.164,
* attach the availability response showing the slot still listed.

Send those to support with the tenant's organization name — the operator
reads the logged booking failure straight off the request id and compares it
against the queue's dispatcher counters.

## See also

* [Public queue self-booking](/guides/queue-appointment-booking) — the
  guide that opts a queue into public booking, publishes its bookable
  window, and composes the hosted booking page URL.
* [Queue capacity gates](/troubleshooting/queue-capacity-gates) — the
  sibling queue refusals (`QUEUE_DEPTH_CAP_REACHED`) on inbound call entry,
  a different surface from self-booking.
* [Queue SLA forecast callback gate](/troubleshooting/queue-sla-callback-gate) —
  the `503` + `slaCallbackBlocked` entry refusal and the callback-dispatcher
  wiring that also dials your booked appointments.
* [Voice queues guide](/guides/voice-queues) — queue membership, schedules,
  and the callback dispatcher the booked appointment rides on.
