Skip to main content

Troubleshooting: supervisor takeover, whisper, and barge failures

A supervisor coaching surface fails in one of two ways and never in any other way: the platform could not carry the instruction out (a 500 whose work happens against the voice bridge — a leg-swap, a leg redirect, a Redis fan-out), or the instruction collided with state your side already holds (an already-open takeover, a stale call ID, a supervisor not mapped to the call’s queue). Read the error code off the response and you know which side owns the fix — the 500-class codes are report-and-retry, the state-class codes are resolved on your side with a GET and a PATCH, not with a retry loop.

Code matrix — symptom to fix

Two response classes sit next to this table rather than inside it, because they are not 500s and they are yours to resolve:

Initiate takeover returns 500 — SUPERVISOR_TAKEOVER_FAILED

POST /api/v1/voice/supervisor/takeovers drives the warm handoff: it opens a takeover row, then performs the leg-swap on the voice bridge — drops the agent leg and redirects the supervisor leg into the active bridge so the supervisor becomes the active party. A 500 SUPERVISOR_TAKEOVER_FAILED means the handoff failed at the bridge, not at your request: the takeover row is reverted to requested, the customer and the agent keep talking uninterrupted, and a supervisor.takeover_failed webhook carries the reason. Work it in this order:
  1. Read the webhook reason first. The supervisor.takeover_failed payload’s reason field is the upstream error message verbatim. A leg-not-found reason means the call (or the supervisor’s leg) ended between your POST and the leg-swap — a race you handle in your UI, not a platform defect. A bridge-side reason points at the route.
  2. Check trunk health before retrying. A takeover inherits the call’s route: if the trunk the call rode in on is flapping, the leg-swap fails the same way the original dial would. Open Troubleshooting: SIP trunk registration, health, and failover and confirm the route is green before you spend the retry.
  3. Retry once, with a fresh supervisor leg. If the supervisor’s softphone leg dropped (a hung-up tab, a re-registered device), the old supervisor_call_sid is dead — re-dial the supervisor leg and POST with the new sid rather than reusing the one the 500 came back with.
  4. If the reason is whisper-mode only (whisper_mode: true on the webhook), the scope is narrower: the coach-style join failed, no leg-swap was attempted, and the row goes to cancelled. The same retry rules apply.
The customer never hears a failed takeover. Dead air, one-way audio, or choppy speech after a successful takeover is a different problem — that is a media-path complaint on the swapped legs, and it belongs on Troubleshooting: voice call quality, not here.

List, get, and update read-side failures

SUPERVISOR_TAKEOVER_LIST_FAILED (GET list), SUPERVISOR_TAKEOVER_GET_FAILED (GET one), and SUPERVISOR_TAKEOVER_UPDATE_FAILED (PATCH state) are the read and state-advance halves of the same surface. They fail as unexpected 500s — nothing in your request body can cause them:
  • Retry the identical request once. These reads are idempotent: re-issuing a GET never mutates anything, and re-issuing a PATCH with the same target state resolves to the same row when it succeeds.
  • For the PATCH, re-read before you retry. A second SUPERVISOR_TAKEOVER_UPDATE_FAILED in a row, or a 409 CONFLICT, means the takeover already moved — another operator advanced it, the handoff completed on its own, or the call ended. GET the takeover, look at its current state, and only PATCH if the state you want is still ahead of it. PATCHing a terminal row (completed / cancelled) is the 409 case, not a 500.
  • A bare 404 on GET or PATCH is not this class. It means the takeover ID is not yours — nothing retried will change that.
If the 500 persists across identical retries, stop retrying and escalate: capture the request ID from the response (meta.request_id) plus your organization ID, and open a ticket — persistent read-side 500s are a platform condition, not something your request can fix.

Listen, whisper, and barge return 500 — SUPERVISOR_ACTION_FAILED

The per-call primitives — POST /api/v1/voice/calls/{id}/listen, /whisper, /barge, and the silent-monitor twin POST /api/v1/voice/supervisor/calls/{id}/monitor — redirect the supervisor’s leg onto the call. A 500 SUPERVISOR_ACTION_FAILED on any of them is a bridge-side failure, with one tenant-side trigger worth ruling out first:
  1. The leg you named no longer exists. The body carries supervisorCallControlId — the supervisor’s own live leg. If the supervisor hung up, their softphone re-registered, or the call ended in the gap, the redirect targets a dead leg. Re-dial the supervisor leg and retry with the fresh ID before treating it as a platform problem.
  2. Check the trunk route. The redirect inherits the call’s route, so a flapping trunk rejects the same way the original dial would — Troubleshooting: SIP trunk is the route-health check.
  3. Retry once against a live call and a live leg. Persistent 500s after both legs are confirmed live are platform-side — escalate with the request ID.
A 403 FORBIDDEN on these same endpoints is deliberately not a 500: either the supervisor is not mapped to the call’s queue (owner/admin can map them), or the supervisorCallControlId you posted does not belong to a leg your tenant owns — a stale ID from a previous session. Both are request-side; no retry fixes them.

Whisper-text dispatch returns 500 — SUPERVISOR_WHISPER_TEXT_FAILED

POST /api/v1/voice/supervisor/calls/{id}/whisper-text sends a silent, text-only coaching tip to the agent’s on-screen overlay. It never touches the audio path, so a 500 here cannot affect the call — only the tip delivery failed.
  • Retry once. Dispatch is idempotent from the agent’s perspective; at worst they see the tip twice.
  • 500 vs. published: false. A 200 response with "published": false is not a failure — it means the dispatch worked and the agent’s dashboard simply was not subscribed (offline or off-shift). Your UI can render that as “agent offline” rather than an error.
  • 422 INVALID_STATE is permanent for that call: the call has no resolvable agent (a direct-dial call, for instance), so there is no overlay to reach. Do not retry a 422; it cannot succeed.

The supervisor.takeover_failed webhook

supervisor.takeover_failed fires when an initiate-takeover attempt fails (most commonly a bridge error during the leg-swap). It is the record of record for the failure: the takeover row reverts, and the webhook is how your systems learn why. From the webhook events catalog and the payload reference — a delivery looks like:
Read the payload in this order:
  1. reason — the upstream error message verbatim. Leg-not-found wording means the call or the supervisor leg ended mid-handoff; anything else points at the route or the bridge.
  2. whisper_modetrue narrows scope to the coach-style join (no leg-swap was attempted and the row ends cancelled); false means the full warm handoff failed and the row reverted to requested.
  3. id — the takeover ID. PATCH it to cancelled from your side if your UI wants the row closed immediately, or leave it for the supervisor to retry from the wallboard.
Consumer-side posture — match the platform’s delivery semantics:
  • Dedupe on the envelope id. Failed events are the ones most likely to be redelivered (an at-least-once bus retries exactly the deliveries your endpoint missed). Treat the event ID as your idempotency key and drop the duplicate, per Troubleshooting: webhook event dedup.
  • Do not fan the failure back into an immediate re-POST. A webhook-driven “takeover failed → POST again on receipt” loop turns one bridge error into an infinite retry storm. Surface the failure to the supervisor and let a human retry — with a fresh supervisor leg.

What NOT to try

  • Do not re-issue a takeover while one is already open on the call. A second POST against the same call_id is rejected with 409 CONFLICT — an open takeover (any state other than cancelled or completed) makes the instruction ambiguous, and the platform refuses rather than stacking two sessions on one call. Check the active state first: GET /api/v1/voice/supervisor/takeovers?call_id=<id> shows whether a takeover is open; the digital-conversation equivalent for the inbox surface is the get active supervisor takeover state endpoint (GET /api/v1/inbox/conversations/{id}/takeover). Advance the open takeover to completed (or cancel it) with a PATCH, and only then POST again.
  • Do not retry a 500 in a tight loop. The 500 codes above are upstream pass-throughs — the bridge rejected the leg-swap or the lookup failed behind it. A retry after a healthy-route check and a fresh supervisor leg is a good bet; a millisecond-interval retry loop just spends your rate limit on the same rejection.
  • Do not confuse a 409 with a 500. A 409 is your side telling you the state moved (an open takeover, a terminal row) — a GET resolves it. A 500 is the platform side. Retrying a 409 never converges.
  • Do not treat a failed whisper-text call as a call problem. The tip dispatch and the call’s audio are separate paths; a 500 on whisper-text leaves the call itself untouched.

Copy-paste requests

The working shapes, for building or verifying your integration. The initiate request — substitute your live call and supervisor-leg IDs:
A success returns 201 with the takeover row:
reason accepts low_qm_score, agent_struggling, customer_escalation, training, or manual (the default); whisper_grace_seconds is 0–30 (0 skips the whisper phase); whisper_mode: true requests the coach-style join instead of a full handoff and makes agent_call_sid required. The coaching-tip request:
A success returns 200:
text is capped at 500 characters; display_ms is 1 000–30 000 and defaults to 8 000. Remember: published: false with a 200 means the agent was not subscribed — offline or off-shift — not that dispatch failed.

What to capture before escalating

Work the matching section first. If the same code persists after the checks it names, open a ticket with:
  1. The error code and HTTP status, verbatim (SUPERVISOR_TAKEOVER_FAILED, 500).
  2. The request ID from meta.request_id on the failing response.
  3. The takeover ID (takeover_…) if one was created, and the call ID (call_…).
  4. The reason field off the supervisor.takeover_failed webhook delivery, when the failure was on initiate.
  5. Your organization ID (Settings → Organization, or organizationId from GET /api/v1/me).
  6. For the per-call primitives, whether both legs were confirmed live at the time of the retry.

See also