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

# Troubleshooting: supervisor takeover, whisper, and barge failures

> Resolve the SUPERVISOR_* error codes on live-call coaching surfaces — takeovers, whisper-text tips, and the listen/whisper/barge primitives — plus the supervisor.takeover_failed webhook and the already-active-takeover race.

# 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

| Error code                          | HTTP | Endpoint it fires on                                                                                          | What it means                                                                                                                                                            | First move                                                                                                                                                  |
| ----------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SUPERVISOR_TAKEOVER_FAILED`        | 500  | `POST /api/v1/voice/supervisor/takeovers`                                                                     | The warm-handoff sequence (drop the agent leg, redirect the supervisor leg into the bridge) failed against the voice bridge; the takeover row is reverted to `requested` | Check trunk health, then retry once — see [Initiate takeover returns 500](#initiate-takeover-returns-500)                                                   |
| `SUPERVISOR_TAKEOVER_LIST_FAILED`   | 500  | `GET /api/v1/voice/supervisor/takeovers`                                                                      | The takeover list read failed unexpectedly                                                                                                                               | Retry with the same query; if it persists, escalate with the request ID — see [List, get, and update read-side failures](#list-get-and-update-failures)     |
| `SUPERVISOR_TAKEOVER_GET_FAILED`    | 500  | `GET /api/v1/voice/supervisor/takeovers/{id}`                                                                 | The single-takeover read failed unexpectedly                                                                                                                             | Same as above; a 404 `NOT_FOUND` on the same endpoint is a different class — the takeover ID does not exist in your tenant                                  |
| `SUPERVISOR_TAKEOVER_UPDATE_FAILED` | 500  | `PATCH /api/v1/voice/supervisor/takeovers/{id}`                                                               | Advancing the state machine (or cancelling) failed unexpectedly                                                                                                          | Re-read the row before retrying — a 409 `CONFLICT` on the same endpoint means the takeover is already in a terminal state                                   |
| `SUPERVISOR_WHISPER_TEXT_FAILED`    | 500  | `POST /api/v1/voice/supervisor/calls/{id}/whisper-text`                                                       | The coaching-tip dispatch failed unexpectedly (the publish layer, not the call)                                                                                          | Retry once; the call is unaffected — see [Whisper-text dispatch returns 500](#whisper-text-dispatch-returns-500)                                            |
| `SUPERVISOR_ACTION_FAILED`          | 500  | `POST /api/v1/voice/calls/{id}/{listen,whisper,barge}` and `POST /api/v1/voice/supervisor/calls/{id}/monitor` | The per-call supervisor primitive failed against the voice bridge (a stale leg reference after the call ended is a common trigger)                                       | Confirm the call and the supervisor leg are still live, then retry once — see [Listen, whisper, and barge return 500](#listen-whisper-and-barge-return-500) |

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

| Response            | Where it appears                                        | What it means                                                                                                                  | Fix                                                                                                                     |
| ------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `409 CONFLICT`      | `POST /api/v1/voice/supervisor/takeovers`               | A takeover on the same call is already open (any state except `cancelled` / `completed` keeps it open)                         | Find the open takeover, advance or cancel it, then POST again — see [Already-active takeover](#already-active-takeover) |
| `404 NOT_FOUND`     | POST takeover; PATCH takeover; POST whisper-text        | The call ID (or takeover ID) does not exist in your tenant — a typo, an ended call whose row aged out, or a stale UI reference | Pull the live ID from your active-call list and retry against it                                                        |
| `422 INVALID_STATE` | `POST /api/v1/voice/supervisor/calls/{id}/whisper-text` | The call has no resolvable agent target (a direct-dial call with no agent on it)                                               | There is nobody to receive the tip — this is a permanent rejection for that call, not a transient failure               |
| `403 FORBIDDEN`     | All supervisor surfaces                                 | Supervisor role not mapped to the call's queue, or a supervisor leg that belongs to another session                            | Ask an owner/admin to map the supervisor to the queue, or re-dial the supervisor leg                                    |

## 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](/troubleshooting/sip-trunk)
   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](/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](/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](/webhooks/events) and the
[payload reference](/reference/webhook-events) — a delivery looks like:

```json theme={null}
{
  "type": "supervisor.takeover_failed",
  "data": {
    "id": "takeover_9f2c1a7e",
    "call_id": "call_abc123",
    "reason": "upstream bridge rejected the leg swap",
    "whisper_mode": false,
    "organization_id": "org_abc"
  }
}
```

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_mode`** — `true` 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](/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](/api-reference/endpoints/inbox)
  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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/supervisor/takeovers" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "call_id": "call_abc123",
    "supervisor_call_sid": "9f3c2a1b-supervisor-leg",
    "agent_call_sid": "8e2b1c4d-agent-leg",
    "agent_being_replaced": "user_agent_7",
    "reason": "customer_escalation",
    "whisper_grace_seconds": 5,
    "whisper_mode": false
  }'
```

A success returns `201` with the takeover row:

```json theme={null}
{
  "data": {
    "id": "takeover_9f2c1a7e",
    "callId": "call_abc123",
    "conversationId": null,
    "initiatedBy": "user_supervisor_1",
    "agentBeingReplaced": "user_agent_7",
    "reason": "customer_escalation",
    "state": "whisper_phase",
    "whisperStartedAt": "2026-09-07T14:02:11.000Z",
    "whisperUntilAt": "2026-09-07T14:02:16.000Z",
    "handoffStartedAt": null,
    "takeoverCompletedAt": null,
    "contextPreserved": {},
    "notes": null,
    "createdAt": "2026-09-07T14:02:11.000Z"
  },
  "meta": {
    "request_id": "req_…",
    "timestamp": "2026-09-07T14:02:11.000Z"
  }
}
```

`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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/supervisor/calls/call_abc123/whisper-text" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Ask for the account number once more", "display_ms": 8000 }'
```

A success returns `200`:

```json theme={null}
{
  "data": {
    "call_id": "call_abc123",
    "action": "whisper_text",
    "agent_user_id": "user_agent_7",
    "published": true,
    "publish_error": null,
    "display_ms": 8000
  },
  "meta": {
    "request_id": "req_…",
    "timestamp": "2026-09-07T14:04:02.000Z"
  }
}
```

`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

* [Troubleshooting: voice call quality](/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](/troubleshooting/sip-trunk) —
  the route-health check a takeover retry inherits.
* [Troubleshooting: webhook event dedup](/troubleshooting/webhook-event-dedup) —
  the idempotent-consume posture the failed-event webhook needs.
* [Webhook events catalog](/webhooks/events) — the full supervisor
  event set (`takeover_requested` / `completed` / `failed` /
  `cancelled`).
* [Webhook events reference](/reference/webhook-events) — payload
  samples for the same four events.
* [Error codes reference](/reference/error-codes) — every code this
  page maps, in the platform-wide catalog.
