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

# Voice queue callbacks with SLA policies and scheduling

> Offer callers a callback instead of hold, let them pick ASAP or a scheduled window, give them a self-service link to cancel or move the appointment, and gate callback backlog on a per-queue SLA policy with breach alerts.

# Voice queue callbacks with SLA policies and scheduling

A voice queue callback lets a waiting caller swap hold time for a dial-back: the platform remembers their request, calls them when an agent frees up (or at a time they picked), and hands them a tracking link they can use to cancel or reschedule without ever calling you back. On top of that, a per-queue **callback SLA policy** turns "how old may the oldest pending callback get before someone is paged" into a stored objective with automated breach alerts — so an aging callback never silently expires.

Callbacks in Orbit come in two complementary flavors:

* **Overflow (time-driven) callbacks** — offered after the caller has waited past the queue's overflow threshold. The dispatcher retries on a backoff ladder until the callback connects or exhausts the queue's retry budget. These rows surface under `GET /api/v1/voice/callbacks` in the dashboard's **Voice → Callbacks** page.
* **In-queue (position-saving) callbacks** — offered while the caller is still holding, sometimes before the breach (the [pre-suggest SLA forecast](/voice/sla-breach-forecast-callbacks) can raise the offer early). The dispatcher waits for agent availability rather than a timer, so the caller keeps an implicit place in line. These rows surface under `GET /api/v1/voice/callback-in-queue`.

Both flavors dispatch exclusively over the Devotel softswitch — you never configure a third-party outbound trunk to activate callbacks.

**Base path:** `/api/v1/voice`

**Authentication:** Clerk session or API key with the `voice` scope (`voice:write` for mutations, `voice:read` for monitoring).

***

## 1. In-queue callback options, public tokens, and opt-out

A caller consents to a callback inside the queue's IVR (press 1 when the offer plays), and that consent lands in one of two stores depending on which flavor offered it:

* the overflow store, or
* the in-queue, position-saving store.

The consent call itself is platform-internal — your IVR does not call it directly — and it happens **before** the caller hangs up, so the saved position survives the disconnect. Both stores record:

* the queue the caller was holding on,
* the masked + full E.164 caller id,
* the offer-time estimated wait (used to compare the promise against actual dispatch latency),
* the caller's preferred callback channel (`voice` default, or `sms` for an async text-back preference — the SMS hookup still exits over the Devotel softswitch like every other outbound leg),
* and, for scheduled callbacks, an explicit appointment window or single target time (ISO 8601 with timezone).

**Duplicate-consent guard.** If the same call consents twice (a jittered press-1, an IVR retry), the second insert returns **409 Conflict** and no duplicate row is created — the caller never receives two callbacks for one hold.

### Opting callers out

There are three distinct opt-outs, each tenant-owned:

1. **The caller cancels from the public link** (next section).
2. **An operator cancels from the dashboard** — the **Voice → Callbacks** page lists every pending row with a Cancel action (`DELETE /api/v1/voice/callbacks/:id` or `DELETE /api/v1/voice/callback-in-queue/:id`). Canceling an already-connected or already-terminal row returns 204 and is audit-logged as a no-op, so a dispute can still prove the cancel attempt was made.
3. **A queue stops offering callbacks entirely** — the offer lives in the queue's IVR/routing config, so removing the offer prompt from the IVR branch stops new consents without touching any pending rows.

### Public self-service tokens

A caller cannot be expected to hold a Clerk session, so the tracking link they receive uses a **signed token** instead of a login. When the dispatcher starts dialing, the caller gets a "your callback is on its way" SMS containing a link of the form `…/api/v1/public/callbacks/<token>`. The token is an HMAC-SHA256 envelope carrying the callback id and owning tenant schema — no session, no signed-in user, and no way to pivot across tenants by editing the URL (any tamper fails the constant-time compare).

The token is valid for **7 days**, comfortably longer than any live callback's dispatch envelope. It opens a self-service surface:

| Action                         | URL                                                                                   |
| ------------------------------ | ------------------------------------------------------------------------------------- |
| Status landing page (HTML)     | `GET /api/v1/public/callbacks/<token>`                                                |
| One-tap cancel (HTML tap link) | `GET /api/v1/public/callbacks/<token>?action=cancel`                                  |
| JSON status                    | `GET /api/v1/public/callbacks/<token>/status`                                         |
| JSON cancel                    | `POST /api/v1/public/callbacks/<token>/cancel`                                        |
| JSON reschedule                | `POST /api/v1/public/callbacks/<token>/reschedule` with body `{ "at": "<ISO 8601>" }` |

Every mutation is guarded so a row that already left the `pending` state (dialing, connected, or terminal) is never altered — a re-tap returns "already canceled" rather than an error, so the caller's Second tap never breaks anything. These endpoints are rate-limited per IP like every other public surface.

## 2. Configure the callback-in-queue surface

**Dashboard.** Open **Voice → Queues**, pick the queue, and confirm the queue actually voices the in-queue offer (press 1) to a waiting caller — the offer prompt is part of the queue's IVR path. The list view at **Voice → Callbacks** then starts streaming rows for that queue.

**API.** Three surfaces make up the operator workflow:

```bash cURL theme={null}
# List pending callbacks with a summary (counts + average promised wait)
curl "https://api.orbit.devotel.io/api/v1/voice/callbacks?queue_id=queue_supp&status=pending&limit=50" \
  -H "X-API-Key: dv_live_sk_your_key_here"

# List in-queue (position-saving) callbacks separately
curl "https://api.orbit.devotel.io/api/v1/voice/callback-in-queue?queue_id=queue_supp" \
  -H "X-API-Key: dv_live_sk_your_key_here"

# Cancel a pending row (idempotent — a second cancel is a 204 no-op)
curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/callbacks/cbr_0123456789abcdef" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The list endpoints mirror each other's shape: `?queue_id`, `?status` (`pending | dialing | connected | failed | abandoned | canceled`), cursor pagination (`?page_cursor`, `?limit` up to 100), and the overflow list additionally accepts `?channel=voice|sms`. The response returns `items[]`, a `next_cursor`, and a bounded `summary` (total / successful / failed / pending / `avg_wait_seconds`) computed over the most recent slice so a 24/7 queue never pushes an unbounded aggregate into a wallboard poll.

Two operator-control mutations exist on the overflow surface (`PATCH /api/v1/voice/callbacks/:id`):

* `{ "action": "retry_now" }` — re-enqueue the row for **immediate** dispatch (the retry ladder restarts on the current tick). Returns 409 if the row is already dialing or terminal.
* `{ "action": "reprioritize", "new_position": 1 }` — pull the row ahead of every other pending FIFO entry. The position number is the requested rank; the server clamps it to the current front of the queue.

Both are rate-limited under the auth-write budget and require a `voice:write` plus an owner/admin/developer role — a `voice:read` key cannot replay or cancel a callback.

### Scheduled callbacks (appointment slots and windows)

The consent request accepts three scheduling shapes, all ISO 8601 **with timezone offset**:

| Field                                             | Shape                                                   | Effect at dispatch                                                                                                                           |
| ------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `scheduled_window_start` + `scheduled_window_end` | a window ("anywhere between 2 PM and 4 PM")             | the row sits in FIFO until the window opens; past `scheduled_window_end` it fails with `MISSED_SCHEDULED_WINDOW`                             |
| `scheduled_for`                                   | a single-point appointment ("call me at 3 PM tomorrow") | bounded to at most 14 days out and never more than 1 minute in the past; this is the same column the self-booking appointment surface writes |
| (neither)                                         | ASAP                                                    | legacy behavior — the retry ladder starts now                                                                                                |

The two window fields **must be set together or both omitted** — a single-sided window is a 400. The self-book surface lives at `GET /api/v1/voice/queues/:queueId/availability` (published slots derived from the queue's business-hours schedule minus already-booked slots) plus `POST /api/v1/voice/queues/:queueId/appointments`, and it persists as a scheduled callback row on the **same** `callback_requests` store the rest of this page covers — no second dispatcher to learn.

## 3. SLA policies and escalation

Before the SLA policy surface existed, a wallboard could only flag an aging callback by passing threshold query params on every poll. The per-queue **callback SLA policy** turns that into a persisted objective:

```bash cURL theme={null}
# Persist a per-queue callback SLA policy
curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/queues/queue_supp/callback-sla/policy" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "maxOldestAgeSeconds": 900,
    "maxPendingCallbacks": 5,
    "notifyEnabled": true,
    "notifyKind": "both"
  }'
```

| Policy field          | Meaning                                                                       | Bounds                     |
| --------------------- | ----------------------------------------------------------------------------- | -------------------------- |
| `enabled`             | master switch — a disabled or absent policy gates nothing and alerts nothing  | boolean                    |
| `maxOldestAgeSeconds` | age gate on the oldest pending-due callback (across **both** callback stores) | 0 = disable the age gate   |
| `maxPendingCallbacks` | pending-count gate across both stores                                         | 0 = disable the count gate |
| `notifyEnabled`       | whether a breach fans out to the notify/webhook surfaces                      | boolean                    |
| `notifyKind`          | `bell` (org-admin bell/email), `webhook`, or `both`                           | enum                       |

The counter-posed read is the **ad-hoc** snapshot, useful when you want a one-off verdict without persisting a policy:

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/queues/queue_supp/callback-sla?max_age_seconds=1800&max_pending=5" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

It returns the current oldest-callback age + pending count (summed across the overflow and in-queue stores) together with a breach verdict. Rows scheduled for a future moment (a caller-booked tomorrow-at-3pm appointment) are excluded from the age calculation so a legitimately future appointment never trips today's SLA. The wallboard's **live SSE stream** emits the matching `queue.callback_sla_breach` frame when the supervisor passes thresholds on the stream's query.

### Breach-scan escalation

With a persisted policy, a supervisor stops polling and lets the server drive the fan-out:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/queues/queue_supp/callback-sla/breach-scan" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The scan resolves the queue's persisted policy, evaluates it against live callback metrics, and — on a breach with `notifyEnabled` — routes to the surface(s) named in `notifyKind`:

* **bell** — org-admin in-app bell + email via the registered `voice_queue_alert_rule_fired` notification kind,
* **webhook** — an outbound `queue.callback_sla_breach` tenant webhook (registered under your webhook subscriptions),
* **both** — the two above.

The response echoes the evaluation verdict plus which surfaces actually fired, so a supervisor testing the alarm sees the whole chain in one call. Deletes are symmetric — `DELETE …/callback-sla/policy` clears a queue's policy and the queue goes quiet. The org-level rollup at `GET /api/v1/voice/sla-hub` aggregates every policy-bearing queue's callback SLA policy plus the newest-first queue breach history in one response.

## 4. PSAP and AI callbacks

Two adjacent callback surfaces address the lifecycles the queue core doesn't.

### PSAP emergency-callback routing

When a device dials an emergency code and the call drops, the incoming **PSAP dispatcher's** callback must reach the exact extension that called — not the IVR, not the ACD queue. The per-DID emergency-call log records who called against the DID that called back, and the inbound router consults the resolver first:

| Route                                                                | Purpose                                                                                                         |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `POST /api/v1/voice/psap-callback`                                   | record an emergency call against its callback DID (the emergency-dial path calls this)                          |
| `GET /api/v1/voice/psap-callback/:numberId`                          | list the emergency-call log for a DID                                                                           |
| `GET /api/v1/voice/psap-callback/:numberId/resolve?window_seconds=…` | resolve the callback target for an inbound call to this DID — non-match falls through to normal inbound routing |
| `DELETE /api/v1/voice/psap-callback/:numberId`                       | clear the log (reset)                                                                                           |

Org ownership is enforced per DID so a sibling sub-org cannot read or mutate another sub-org's emergency-call log. This is inbound routing metadata only — it never places or terminates a call.

### AI voicemail / missed-call callbacks

An inbound route can opt into an **AI-agent auto-callback**: the moment a voicemail is captured (or a genuine missed call terminates with no voicemail left), the platform places the callback through the same AI-agent outbound service a human click-to-call uses — no operator click required. The per-route `aiCallbackAgentId` field on the inbound-route config carries the opt-in, and every existing outbound guard (quiet-hours, DNC, frequency caps, balance, per-agent outbound enablement) applies unchanged. Failures never block voicemail capture — the auto-callback is fail-soft by design and degrades to a log line.

A supervisor can also rescue an abandoned queue call directly from the wallboard: `POST /api/v1/voice/supervisor/callbacks` with `{ "abandoned_call_id": "…" }` reads the caller's ANI and inserts a pending callback row the existing dispatcher picks up on the next tick — the TCPA federal calling window is respected and the row's first attempt defers if the recipient is currently outside it. Supervisor-role users may only rescue calls in queues they supervise; owner/admin surfaces bypass that gate.

## 5. Verification sequence

Run this in order on a fresh queue to prove the whole chain:

1. **Configure the offer.** On **Voice → Queues**, confirm the queue voices the in-queue press-1 offer and that `targetServiceLevelSeconds` is set so the SLA forecast/gate can flag heads nearing the breach (see [the SLA forecast guide](/voice/sla-breach-forecast-callbacks)).
2. **Consent while holding.** Place a test call, let it wait, press 1 when the offer plays, hang up. A row should appear under `GET /api/v1/voice/callback-in-queue?queue_id=…` with status `pending` and a masked caller id.
3. **Consent after overflow.** Place a second test call, let it blow past the queue's overflow threshold, consent, hang up. A row appears under `GET /api/v1/voice/callbacks?queue_id=…` — and the dashboard's **Voice → Callbacks** page shows both queues' lists.
4. **Exercise the public token link.** When the dispatcher dials, the delivered SMS link resolves `GET /api/v1/public/callbacks/<token>` — confirm the status page reflects the row, and `?action=cancel` transitions it to `canceled`.
5. **Exercise the operator mutations.** `PATCH /api/v1/voice/callbacks/:id` with `{ "action": "retry_now" }` returns 200 immediately after a fresh consent; a second `retry_now` on a `dialing` row returns 409.
6. **Persist an SLA policy + scan.** Run the `PUT …/callback-sla/policy` call from section 3, then `POST …/callback-sla/breach-scan`; the response names which notify surface fired, and the org-admin bell shows the alert. `GET /api/v1/voice/sla-hub` rolls the result up with every other queue's policy.
7. **Rescue an abandoned call.** Place a call, hang up before an agent picks up (so the queue logs it `abandoned`), then `POST /api/v1/voice/supervisor/callbacks` with that abandoned call id — the row appears in the same pending list the rest of the dispatcher drains.

Any failed step points at the section that owns it: (1–3) at the queue/IVR configuration, (4) at public-token resolution, (5) at operator permissions, (6) at the policy store, and (7) at the supervisor-rescue seam.

## Related

* [Inbound queue SLA forecast: pre-suggest callback before breach](/voice/sla-breach-forecast-callbacks) — the advisory wave that flags heads for the press-1 offer before the queue's SLA actually misses.
* [Inbound queue SLA forecast + virtual callback gate](/voice/queue-sla-forecast-callback) — the queue-entry gate that blocks entries outright on an already-breaching queue and steers them to the virtual callback.
* [Set up and run voice queues](/guides/voice-queues) — create the queue, set the SLA target, and wire the alert rules the callback SLA breach rides on.
* [Wallboard alarm rules](/guides/wallboard-alarm-rules) — the supervisor alert surface that complements the callback SLA policy's bell/webhook fan-out.
