Skip to main content
examples/salon-concierge carries the cold half of this on its LiveKit target, over a Twilio SIP trunk. Warm transfer compiles on no other route today, and no shipped example declares it, so the warm snippets below are worked examples rather than quotes from a package you can run. On this page:
targets.yaml
agent.yaml
The route is connections/twilio_sip.yaml, below. The two desks are in agent.yaml, because the same desk answers whichever target places the call. Each one names an environment variable rather than a number, a rule Human transfers states in full.

Two transfers, side by side

agent.yaml
Cold is a SIP REFER. The agent asks the carrier to hand the caller’s existing leg somewhere else. The caller leaves the room and the session ends. If the REFER fails, the caller stays with the agent and on_unavailable applies. Warm uses LiveKit’s own prebuilt warm transfer task. The caller waits on hold, the supervisor’s line rings, the agent briefs them, and only then are the two connected. briefing is free text, not a mode: the call transcript is passed along on its own, so write what the person needs on top of it. Every failure, no answer, decline, voicemail, failed dial, comes back as one error and on_unavailable decides. The person who answers hears the handover in the first sentence: who is on hold, what they want, what was tried, then one question they can answer. If the conversation has too little detail, the agent says that plainly instead of inventing a briefing.

Every key a transfer block takes

destination: is the only key either block requires. The entry’s own when:, and the choice between cold: and warm:, work the same on every route and are stated on Human transfers.
a destinations entry name
required
Which desk to reach. The name is resolved in the destinations: block above, so the model never sees a number.
string
What to tell the person while the caller waits. It belongs to a warm: block and is refused in a cold: one. Left out, the person still gets the transcript, which rides along on its own.
a positive Go duration, e.g. 25s
How long the destination rings before it counts as unavailable. It reaches both shapes on this route. Left out, LiveKit is sent no value and its own platform default stands.
return_to_caller | hangup
default:"return_to_caller"
What happens when the destination does not take the call. On this route no answer, a decline, voicemail and a failed dial all arrive as one failure, so one setting covers them.
ring_timeout covers ringing only. Once the person answers, LiveKit has no post-answer timeout and the caller stays on hold until the consultation ends. The agent asks again when the person does not decide, but that prompt is a mitigation rather than a hard time limit.

The trunk has to allow it

Cold transfer is a SIP REFER, so the trunk must permit transfers. In the trunk’s General settings, three things have to be right, and a new trunk has none of them: Twilio’s transfer mode has a third value, sip-only, which allows SIP destinations and refuses PSTN ones. A phone number destination needs the PSTN box ticked. Caller ID decides whether the transfer connects at all. Transferee presents the original caller’s number to the destination, which is the nicer behaviour and works where the receiving carrier accepts a number your account does not own. Transferor presents your own Twilio number, and is what to use when the transfer crosses a border. Getting this wrong fails every transfer instantly, with 486 Busy Here and a zero second, zero cost call leg in Twilio’s log. The caller ID section shows how to tell that apart from a destination that is genuinely busy. Caller ID for the transfer target is that trunk setting, never per call. Transfers to emergency numbers are not supported, and the referred leg keeps billing per minute trunking charges. LiveKit Phone Numbers cannot transfer. Use a SIP trunk whose provider supports REFER; this example uses Twilio Elastic SIP Trunking.

The connection

Four standard SIP names, not Twilio specific ones, because the same generated code dials through any SIP carrier with them:
connections/twilio_sip.yaml
The first two lines are the route: this is the file that says the call arrives over SIP and Twilio carries it. The four names below are yours, and the compiler carries whatever you write through verbatim. Those settings belong to the route rather than to this page. Which keys each route accepts is in Connection configuration. Where each Twilio value is found is in LiveKit over Twilio.

Run it

The build carries the route, every required environment variable, and the transfer capability the route supports. The emitted README.md walks the trunk setup for it. Inbound on this route is tested against a deployment: SIP needs the carrier to reach signalling and media at a routable address, which a laptop behind a home router cannot offer. Deploy the generated project, finish the trunk setup above, and call your number to reach a transfer. Cold transfer needs an existing SIP caller leg, so it cannot be tested from a browser session or the LiveKit Agent Console. Warm transfer can start there, because it dials the person itself. That difference also controls startup checks. A cold destination environment name is required only after a real SIP job is identified, before the greeting; it does not block WebRTC startup. Warm transfer is always available to the browser agent, so its destination and the selected connection’s SIP address, username, password, and caller number remain required in REQUIRED_ENV and compose.dev.yaml. Keep lk agent logs open. A warm transfer prints the control, the conversation message count handed to the briefing, and then either warm transfer merged or warm transfer unavailable. A cold transfer prints either cold transfer completed, cold transfer failed, or cold transfer skipped: no phone caller in the room. A dial line with no final warm line means the consultation is still running.

Where to go next

Pipecat on Twilio

Cold transfer on Pipecat with nothing hosted by you.

Targets

Compare Pipecat, LiveKit, and SLNG.