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

# AI-voice warm handoff to a human agent

> Wire the transfer_to_human tool on your voice agent so an escalation bridges the live caller into an ACD queue and the receiving human sees a screen-pop with the AI's summary, sentiment, and collected data.

# AI-voice warm handoff to a human agent

When an AI voice agent hits something it cannot resolve — a caller asks for a person, an account action needs approval, the caller is frustrated — the worst outcome is a cold hangup followed by a redial. The warm handoff keeps the caller connected: the call is bridged straight into one of your ACD queues, and the human agent who picks up sees a screen-pop containing an AI summary of the call so far, caller sentiment, key topics, action items, and every piece of data the bot already collected. The caller never has to repeat themselves.

This guide covers the inbound **AI → human** bridge end to end: the `transfer_to_human` tool, the handoff packet, the read endpoint the agent desktop uses, and the internal mirror the agent runtime calls. For outbound-dialer overflow (routing excess campaign volume to humans), see [Live-human overflow for AI voice-agent campaigns](/guides/outbound-live-human-overflow).

***

## 1. The handoff contract

Two tenant-facing endpoints drive the handoff:

* `POST /api/v1/voice/calls/:id/ai-handoff` — bridge a live call into an ACD queue and stamp the handoff packet onto the call record.
* `GET /api/v1/voice/calls/:id/ai-handoff` — read the persisted packet for the screen-pop.

The POST is invoked by the agent runtime when the voice agent invokes the `transfer_to_human` tool (see §5 for how that reaches the API), or by your own integration when you build a custom escalation path. It accepts `queue_id` (the ACD queue to enqueue into) and a free-text `reason` for the escalation. Optional fields include `collected_slots`, `priority`, `required_skills`, and the `conference_id` when the bot is in a multi-party session.

The orchestration runs in a fixed order so the caller never loses audio:

1. Generate (or reuse the cached) AI call summary.
2. Assemble and persist the handoff packet onto the call record.
3. Enqueue the live caller into the ACD queue — the warm bridge.
4. Drop the bot leg from the conference, only after the caller is safely queued.

Routing back into your own ACD queue means no outbound leg is dialled; the caller stays on the line they already have.

## 2. Enable the `transfer_to_human` tool on the voice agent

`transfer_to_human` is one of the built-in tools listed when you create an agent — the built-in name is `transfer_agent` in the agent creation flow ([Creating agents](/agents/creating-agents#built-in-tools)). On a live voice session the tool takes two parameters:

| Parameter  | What it carries                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------- |
| `queue_id` | The ACD queue id to enqueue the caller into. Configure the queue for the agent's escalation path. |
| `reason`   | A 1–2 sentence summary of why the agent is escalating, so the receiving human has context.        |

Write an escalation rule into the agent's instructions — for example, "if the caller asks for account unlock, verify their member ID, then call `transfer_to_human`". By the time the tool fires, any slots the agent filled (member ID, email, intent) travel into the handoff packet as `collected_slots`.

For tenant-specific escalation logic — picking a queue by business hours, checking VIP status before choosing a skill group — wrap the decision in a custom tool that resolves the queue and have the agent pass the result to `transfer_to_human`. See [Custom tools](/guides/agents-custom-tools).

## 3. What's in the handoff packet

The packet is a versioned JSON blob stored on the call record, and the agent desktop renders it as the screen-pop on pickup.

| Packet field           | Where it lands on the screen-pop                                                                                                                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`              | Schema marker (`1`) — readers can version-gate before rendering.                                                                                                                                              |
| `reason`               | The escalation headline, shown at the top of the pop.                                                                                                                                                         |
| `summary`              | The AI call summary paragraph; `null` when no usable transcript existed yet.                                                                                                                                  |
| `sentiment`            | The rolling sentiment label (for example `frustrated`, `positive`), when available.                                                                                                                           |
| `key_topics`           | Topic bullets from the summarizer.                                                                                                                                                                            |
| `action_items`         | Follow-up bullets the human should close.                                                                                                                                                                     |
| `collected_slots`      | Structured data the bot gathered (member ID, intent, email) — rendered as labeled fields.                                                                                                                     |
| `transcript`           | Speaker-labeled transcript at the moment of handoff, so the human can read the full conversation inline. Capped in size; `transcript_truncated` flags a cut and the full transcript stays on the call record. |
| `ai_agent_id`          | Which AI agent handled the call before escalating.                                                                                                                                                            |
| `summary_generated_at` | When the summary was produced.                                                                                                                                                                                |
| `handed_off_at`        | When the handoff was orchestrated.                                                                                                                                                                            |

Every summary field is fail-open: if the call had no usable transcript at escalation time, `summary` and `transcript` are `null` and the handoff still completes — a missing summary never blocks the bridge.

## 4. Reading the packet back: the GET endpoint

The agent desktop calls `GET /api/v1/voice/calls/:id/ai-handoff` when the dispatcher matches the caller to a human agent, so the screen-pop renders at pickup. It returns:

* `200` with `handoff: <packet>` for a call that was AI-escalated.
* `200` with `handoff: null` for a real call that was never AI-escalated — the common case, not an error.
* `404` only when the call id itself does not resolve in your tenant.

Poll once at pickup; the packet is stamped once at handoff time and does not change.

## 5. How the internal mirror fits

The tenant-facing POST is gated behind dashboard/API auth, so the live bot itself cannot call it. The agent runtime instead calls the machine-to-machine twin:

* `POST /api/v1/internal/voice/ai-handoff` — same orchestrator, same packet, authenticated by an internal service token instead of a user session.

You only call this endpoint if you operate your own voice-agent runtime against Orbit and need to hand off directly. For dashboard-built agents the runtime fires it automatically when the agent invokes `transfer_to_human`. Treat it as infrastructure: no sandbox key, no customer API key — internal token only.

## 6. Warm handoff vs supervisor takeover

This guide covers the **Agent → Agent** (AI → human) bridge: the AI decides to escalate and the caller moves into a queue. It is a different operation from supervisor-driven monitoring, where a supervisor actively snoops, whispers, or takes over a call a human agent is handling. If a supervisor action fails, see [Supervisor takeover failures](/troubleshooting/supervisor-takeover-failures). The AI warm handoff and supervisor takeover never interact — a handoff packet on the call record does not change what a supervisor can do with the call.

## 7. Worked example: the account-lock escalation

An inbound caller reaches your AI concierge. The concierge verifies their member ID, determines the account is locked, and decides a human must handle it.

**1. The agent invokes the tool.** With the built-in tool name `transfer_agent` enabled on the agent, the voice session receives the tool call as `transfer_to_human` with:

```json theme={null}
{
  "queue_id": "qu_91f2support",
  "reason": "Caller verified member ID 8842; account is locked and needs a manual unlock by support."
}
```

**2. Direct API invocation (optional).** If you drive the handoff from your own integration instead, POST to the tenant-facing endpoint:

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/voice/calls/call_8f3e2a1b/ai-handoff" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "queue_id": "qu_91f2support",
    "reason": "Account locked; verified member ID needs manual unlock.",
    "collected_slots": {
      "member_id": "8842",
      "intent": "account_unlock"
    }
  }'
```

The endpoint returns `202` with the queue position, whether a summary was generated, whether the bot leg was dropped, and the full packet:

```json theme={null}
{
  "data": {
    "call_id": "call_8f3e2a1b",
    "queue_id": "qu_91f2support",
    "queue_position": 1,
    "summary_generated": true,
    "bot_dropped": true,
    "handoff": {
      "version": 1,
      "reason": "Account locked; verified member ID needs manual unlock.",
      "summary": "Caller asked about a locked account. Agent verified member ID 8842 and confirmed the lock; a manual unlock by support is required.",
      "sentiment": "neutral",
      "key_topics": ["account lock", "member verification"],
      "action_items": ["Unlock the account for member 8842"],
      "collected_slots": {
        "member_id": "8842",
        "intent": "account_unlock"
      },
      "transcript": "Agent: Thanks for calling…\nCaller: My account is locked…",
      "transcript_truncated": false,
      "ai_agent_id": "agt_3f9cconcierge",
      "summary_generated_at": "2026-09-18T10:24:11Z",
      "handed_off_at": "2026-09-18T10:24:11Z"
    }
  }
}
```

**3. The screen-pop renders.** When a support agent accepts the queued call, the desktop reads the packet with the GET endpoint and renders the reason, summary, sentiment chips, collected-slot fields, and the transcript — the human picks up knowing everything the bot learned.

## Troubleshooting

* **422 with `queue_id is required`** — the tool fired without a queue id. Configure the escalation queue and re-run the agent simulation.
* **404 on the GET** — the call id is not in this tenant's call records; check you are reading with the same tenant key that owns the call.
* **`handoff: null` on the GET** — the call exists but no AI escalation was recorded. Either the tool never fired (check the agent's tool-call history) or the POST failed upstream (check the agent runtime logs for a non-2xx from the internal endpoint).
