Skip to main content

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 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: 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:
cURL
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: 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:
cURL
The counter-posed read is the ad-hoc snapshot, useful when you want a one-off verdict without persisting a policy:
cURL
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:
cURL
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: 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).
  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.