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:
- Read the webhook reason first. The
supervisor.takeover_failedpayload’sreasonfield 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. - 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.
- 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_sidis dead — re-dial the supervisor leg and POST with the new sid rather than reusing the one the 500 came back with. - If the reason is whisper-mode only (
whisper_mode: trueon the webhook), the scope is narrower: the coach-style join failed, no leg-swap was attempted, and the row goes tocancelled. The same retry rules apply.
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_FAILEDin a row, or a 409CONFLICT, 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 currentstate, 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.
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:
- 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. - 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.
- 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.
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": falseis 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_STATEis 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:
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.whisper_mode—truenarrows scope to the coach-style join (no leg-swap was attempted and the row endscancelled);falsemeans the full warm handoff failed and the row reverted torequested.id— the takeover ID. PATCH it tocancelledfrom your side if your UI wants the row closed immediately, or leave it for the supervisor to retry from the wallboard.
- 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_idis rejected with409 CONFLICT— an open takeover (any state other thancancelledorcompleted) 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 tocompleted(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: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:
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:- The error code and HTTP status, verbatim (
SUPERVISOR_TAKEOVER_FAILED, 500). - The request ID from
meta.request_idon the failing response. - The takeover ID (
takeover_…) if one was created, and the call ID (call_…). - The
reasonfield off thesupervisor.takeover_failedwebhook delivery, when the failure was on initiate. - Your organization ID (Settings → Organization, or
organizationIdfromGET /api/v1/me). - For the per-call primitives, whether both legs were confirmed live at the time of the retry.
See also
- Troubleshooting: voice call quality — dead air or one-way audio after a successful handoff is a media-path complaint, not a takeover failure.
- Troubleshooting: SIP trunk registration, health, and failover — the route-health check a takeover retry inherits.
- Troubleshooting: webhook event dedup — the idempotent-consume posture the failed-event webhook needs.
- Webhook events catalog — the full supervisor
event set (
takeover_requested/completed/failed/cancelled). - Webhook events reference — payload samples for the same four events.
- Error codes reference — every code this page maps, in the platform-wide catalog.