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

# Public queue self-booking

> Publish a hosted, no-login booking page for a contact-center queue. Callers pick a slot from the queue's published availability; the queue calls them back at that time as a scheduled callback.

# Public queue self-booking

Every queue you publish gets a hosted booking page at a public URL — no account, no sign-in:

```text theme={null}
https://orbit.devotel.io/book/<tenantId>/<queueId>
```

A caller opens the link, sees the slots the queue published, picks one, and types the phone number you should call back. The booking stores as a **scheduled callback** with that caller's number and slot, and the queue's callback dispatcher dials them at the appointed time. The booking is the caller's consent to be called back at that slot, and every booking writes a consent record you can review in the audit log.

Two public endpoints back the whole surface, which is why you don't have to host anything — embed the URL, and the branded booking page, the slot picker, and the confirmation are already done.

## Tenant setup — expose a queue as bookable

The surface is off until you opt in per organization. Flip it on with a settings write:

```bash cURL theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/settings/general" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "settings": { "public_appointment_booking": { "enabled": true } } }'
```

The same settings block carries your page chrome as optional fields (hostname-safe: the API accepts only http(s) logo URLs, `#rrggbb` colours, and caps the copy lengths):

```bash cURL theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/settings/general" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "public_appointment_booking": {
        "enabled": true,
        "branding": {
          "logo_url": "https://your-cdn.example.com/logo.svg",
          "primary_color": "#0ea5e9",
          "business_name": "Acme Support"
        },
        "copy": {
          "headline": "Book a callback in under a minute",
          "body": "Pick a slot and an agent will ring you at that time.",
          "footer": "Support hours: Mon-Fri 9-5 GMT"
        }
      }
    }
  }'
```

Omit both and the platform renders its default (no branding changes, no copy changes). Compose the public URL from the building blocks you already have: `https://orbit.devotel.io/book/{ tenantId from GET /settings/general }/{ the queue id }`.

**Turn it back off** by writing `"enabled": false` — the booking page renders the 404 and the booking POST answers 422 with the same shape an unknown tenant gets, so nothing picks apart your inventory (an attacker can't tell a queue that never existed from a queue you closed).

## Publish the queue's bookable window

The page publishes slots only for queues with a **schedule**. Two levels:

* **Per-queue hours** — `businessHours` on the queue itself. Set or override it via `PUT /api/v1/voice/queues/{id}` with the `businessHours` field (IANA timezone + day windows). A queue without a `businessHours` falls back to **the org default**, which you set via `PUT /api/v1/organization/business-hours`.
* A queue with *neither* has no published schedule, the availability endpoint reports `reason: "no_schedule_configured"` with an empty slot list, and the booking POST returns 422 `NO_BOOKABLE_SCHEDULE`.

```bash cURL theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/queues/queue_supp" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "businessHours": {
      "timezone": "Europe/London",
      "days": {
        "mon": { "open": "09:00", "close": "17:00" },
        "tue": { "open": "09:00", "close": "17:00" },
        "wed": { "open": "09:00", "close": "17:00" },
        "thu": { "open": "09:00", "close": "17:00" },
        "fri": { "open": "09:00", "close": "17:00" }
      }
    }
  }'
```

Bookable slots respect `slot_minutes` (5–240, default 30) and `lead_minutes` (minimum notice before a slot, 0–20160, default 60) — the page and API accept both as query overrides, so a booking link can ask for 45-minute slots with 2-hour notice without changing the queue configuration.

## Booking lifecycle

Every booking runs the same four steps:

1. **Availability.** `GET /api/v1/public/queues/{tenantId}/{queueId}/availability` (or the booking page itself, which calls this) enumerates the queue's business-hours window into concrete slots and subtracts the slots already holding a booking, so two customers can't take the same slot.
2. **Book.** `POST /api/v1/public/queues/{tenantId}/{queueId}/appointments` writes one scheduled callback row with the caller's `phone` and chosen `slot_start`.
3. **Dispatch.** At the slot start, the queue's callback dispatcher dials the caller. The dial respects the queue's callback retry/attempt settings, and the row the booking wrote is the same shape as a callback your agents queue from the dashboard.
4. **Cancel or reschedule.** The caller receives a tracking link (`GET /api/v1/public/callbacks/<token>`) in the "your callback is on its way" message — a signed HMAC envelope that carries the callback id, never a tenant id or queue id the caller could tamper with. Status, one-tap cancel, and reschedule all run that same token (see [Callbacks and scheduling](/voice/callbacks-scheduling)). The booking's consent record stays in your audit log either way.

Self-booking via curl, once you know the queue is bookable:

```bash cURL theme={null}
# 1. See what slots the queue publishes
curl "https://api.orbit.devotel.io/api/v1/public/queues/<tenantId>/<queueId>/availability"

# 2. Book one
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-10T14:00:00Z",
    "slot_minutes": 30,
    "phone": "+15551234567",
    "name": "A. Customer",
    "notes": "Billing question"
  }'

# -> HTTP 201 { "data": { "id": "cbq_...", "status": "pending", "queue_id": "queue_supp",
#        "slot_start": "2026-09-10T14:00:00Z", "slot_end": "2026-09-10T14:30:00Z" }, "meta": {...} }
```

A slot someone just took returns `409 SLOT_ALREADY_BOOKED` (the caller picks another); a queue that published no schedule answers `422 NO_BOOKABLE_SCHEDULE`; anything that can't identify the tenant or that hasn't opted in answers `422 INVALID_TENANT`.

## Integration — get the link into a caller's hands

* **SMS deep link.** Send the booking URL in the message. Append `?phone=<E.164>` and the phone field arrives pre-filled, so the caller taps a slot with nothing to type: `https://orbit.devotel.io/book/<tenantId>/<queueId>?phone=+15551234567`.
* **Email footer / ticket link.** Drop the URL into signature blocks or the "can't wait?" line of a support reply; the page renders the branding and copy you configured.
* **QR code** (for in-store / printed collateral). Point the QR at the URL; any phone camera can open it.
* **IVR hand-off** — pair the booking cue with the existing [press-1 virtual callback](/voice/queue-sla-forecast-callback) flow: when the queue offers a callback in-queue, play the booking URL instead and send it by SMS to the caller, so the caller who wants a scheduled slot picks it off the page rather than waiting to be dialled.

Because the surface is tokenless (the URL identifies only tenant + queue, and exists only when you publish the queue), the booking POST is the only call your integration needs to guard. Ready-made booking page means you can use the plain URL from any channel — SMS, WhatsApp, email, chat, or a link from your own web support page.

## Ops

* **No existence signal.** A queue that is closed or unknown returns `{ "enabled": false }` on the availability GET and `422 INVALID_TENANT` on the booking POST — the same body an unknown tenant gets either way. Never verify "is this queue enabled?" from a public reader; verify inside your own systems.
* **Rate limiting.** Every public call to the surface sits under the per-IP public surface limit. If you embed booking into your own agent or routing flow, pick the slot client-side first and then forward the booking POST so a half-finished journey never burns the public budget.
* **SLA and audit inherit the queue.** The queue's SLA target, occupancy, and health metrics read the booking rows the dispatcher dialled, so the booking surface and the manual callback queue share the wallboard you already run. The booking audit row keeps the caller's phone masked (the raw number lives on the callback row for dispatch only), and the booking remains the caller's consent to a callback at the chosen slot.

## Related

* [Callbacks and scheduling](/voice/callbacks-scheduling) — the tracking token's cancel/reschedule surface and the scheduled callback pipeline this page writes into.
* [Queue SLA forecast-callback runbook](/guides/queue-sla-callback-runbook) — the queue-level SLA objective the booked callbacks share.
* [Voice queues](/guides/voice-queues) — create, staff, and tune the queue before you publish it.
