# Transfers

> Hand a call to a person so they arrive knowing what happened and the caller never repeats themselves.

Source: https://docs.vlayers.ai/build/transfers

Kind: How-to guide

## What you'll learn

How to set up a warm transfer in a flow or in code, write the briefing, and allow the numbers code may transfer to. A transfer is the moment your agent is judged.

## Warm and cold transfers

| Type | What happens | Status |
|---|---|---|
| **Warm** (recommended) | The person is dialled into the call. The agent waits for them to answer, briefs them while the caller listens, then leaves. Nobody is put on hold and nothing is repeated. | Available |
| **Cold** | The caller is passed to another number and the agent's call ends. The person picks up with no context. | Not available on VoiceLayer phone lines yet. Use a warm transfer. |

A warm transfer sounds like this:

1. The caller hears: "One moment, I'm bringing our on-call engineer onto the line now."
2. VoiceLayer dials the person and waits for them to pick up.
3. Both hear your briefing, word for word.
4. The caller hears: "You're both connected now. I'll drop off."
5. The agent leaves. The caller and the person stay on the call.

The lines around your briefing are fixed on purpose: a transfer should never be improvised.

## Prerequisites

- A destination in E.164 format, such as `+14155550100`: no spaces, dashes or brackets.
- Someone who answers it. A warm transfer waits for a real answer; if nobody picks up, the call falls back instead of briefing an empty line. Route to a rota or a desk line, not one mobile.
- A caller on a phone line. Web and Playground callers have no phone line to transfer; the agent explains that and ends cleanly.

## Step 1: Write the briefing

The briefing is spoken aloud, word for word, to the caller and the person joining. Write what a colleague would say, and use [variables](https://docs.vlayers.ai/build/variables) to make it specific:

```text
Hey {{ oncall_name }}, I've got {{ caller_name }} from {{ site }} on the line
reporting a {{ severity }} issue. {{ summary }}. The ticket is open and flagged.
They can fill you in on the details. Take it from here.
```

Read it aloud before you ship it. "VENUE Sev-1 escalation, call vl-client-74cc" is a log line. "Hey Marcus, I've got someone from Keystone Arena reporting a Sev 1" is a handoff.

## Step 2: Add the transfer

Flow builder:
Add a **Transfer to human** step, set **Transfer to**, choose **Warm** and write the briefing. Put the transfer after the steps that gather context and open the record, so the person arrives informed.

Voice agent SDK:
Declare the destinations and the transfer capability in the agent, then call `ctx.handoff`:

```ts
defineAgent({
  // ...
  capabilities: ['system.say', 'system.transfer_call'],
  handoff: {
    fallback: '+14155550000',
    routes: { desk: '+14155550001' },
    mode: 'warm',
    briefing: 'Hi, this is the network desk with a caller who needs a person.'
  }
});
```

```ts
// a destination declared in the agent passes as written
await ctx.handoff('+14155550100', {
  reason: 'sev-1 escalation',
  mode: 'warm',
  briefing: `Hey Marcus, I have someone from ${site} reporting an outage. Ticket ${ticketId} is open.`
});

// a number you look up passes only if the workspace's Transfer destinations allow it
const engineer = await rota.onCall(venue);
try {
  await ctx.handoff(engineer.phone, { reason: 'sev-1', briefing });
} catch (err) {
  if (err instanceof HandlerContextError && err.code === 'guard_denied') {
    await ctx.say('I could not connect the engineer, but the ticket is open and they are being paged.');
  } else throw err;
}
```

Let the transfer do the talking: do not say your own "transferring you now" first, or the caller hears it twice. More in [Voice agent SDK](https://docs.vlayers.ai/developers/sdks/voice-agents).

## Step 3: Allow destinations your code looks up

Every transfer leg is checked before it is dialled:

1. **The destination is declared or allowed.** It is either declared in the agent (`handoff.fallback`, `handoff.routes`), or allowed by your workspace's transfer destinations. A number your code looks up at runtime can only pass through the allowed list.
2. **The leg passes the outbound checks:** the fraud destination block list, a rate cap, a wallet hold and the do-not-call check. The leg is billed as part of the call.

A workspace owner sets the allowed list under **Dashboard → Settings → Transfer destinations**: number prefixes (such as `+1415`) and exact numbers. It is set only in the dashboard by a signed-in member, never in code or with an API key, so a handler cannot widen it.

> **Note:** Until the platform's outbound checks are enforcing, transfers requested by code (ctx.handoff, a model or brain tool, a trigger on a code agent) are refused with gate_not_enforcing and nothing is dialled. The Transfer destinations page shows the current state. A warm transfer built on the canvas works today.

## Test it

Call the agent from a real phone and trigger the transfer. Then open the call under **Conversations**: the transcript shows each step inline (dialling, answered, briefed, agent left), so you can see where a handoff went wrong.

## Common problems

| Problem | Fix |
|---|---|
| The transfer did not connect | Check the destination is E.164 and someone answers it. |
| Nothing happens in the Playground | Browser callers cannot be transferred. Test from a phone. |
| `ctx.handoff` throws `guard_denied` (reason `transfer_refused:<code>`) | The destination is neither declared nor allowed, or the outbound checks refused it. The call stays up: catch the error and tell the caller what happens next. |
| The briefing reads ids aloud | Write it for the ear. See [Voice and turn-taking](https://docs.vlayers.ai/build/voice). |

## Next steps

- [Testing](https://docs.vlayers.ai/build/testing): Prove a transfer works before a customer needs it.
- [Conversations and handoff](https://docs.vlayers.ai/developers/handoff): Hand text conversations between your agent, your code and people.
