Chapter 320m

Cold transfers

Cold transfers

Not every transfer needs a warm handoff. When the caller asks for a known department, when you are routing to an IVR system, or when speed matters more than context, a cold transfer gets the job done. The agent tells the caller what is about to happen, the SIP session is redirected, and the caller is connected to the new destination in seconds. This chapter shows you how to implement cold transfers using SIP REFER and LiveKit's transfer APIs.

Cold transferSIP REFERTransfer targets

What you'll learn

  • How cold transfers differ from warm transfers and when to use each
  • How the SIP REFER protocol redirects an active call
  • How to implement cold transfers to external numbers and internal extensions
  • How to handle REFER failures and unsupported targets

When to use cold transfers

Cold transfers are appropriate when:

  • The caller explicitly asks for a department: "Transfer me to sales."
  • You are routing to an automated system (IVR, voicemail) that does not need a briefing.
  • The receiving party can look up the caller's information from their phone number or account.
  • Speed is more important than context — the caller wants to be connected immediately.

Cold transfers are not appropriate when the caller has spent five minutes explaining a complex issue and would need to repeat everything to the next agent. That is what warm transfers are for.

What's happening

The fundamental tradeoff is simple: cold transfers are faster but lose context. Warm transfers preserve context but take longer. In practice, most production systems implement both and choose based on the situation. A transfer to the billing department might be warm; a transfer to the main menu might be cold.

How SIP REFER works

SIP REFER is the protocol mechanism behind cold transfers. When your system sends a REFER message, it tells the SIP infrastructure to redirect the existing call session to a new destination.

1

Agent initiates REFER

Your LiveKit agent sends a SIP REFER request specifying the target URI. This tells the SIP trunk to redirect the caller's session.

2

SIP trunk processes the REFER

The SIP trunk acknowledges the REFER and begins setting up a new call leg to the target destination. The original call is held during this brief transition.

3

Caller is connected to new destination

The SIP trunk completes the new call leg and bridges the caller to the target. The original LiveKit room connection is released. From the caller's perspective, the call continues without interruption.

4

NOTIFY confirms completion

The SIP trunk sends NOTIFY messages back to indicate the transfer status — whether it succeeded, is ringing, or failed. Your system can use these to track transfer outcomes.

REFER support varies by provider

Not all SIP trunk providers support REFER in the same way. Some require REFER to be explicitly enabled on the trunk configuration. Others may not support REFER at all and require you to use a re-INVITE based approach instead. Check your provider's documentation before relying on REFER.

Implementing cold transfers in Python

Cold transfers in LiveKit use the transfer_sip_participant API to send a SIP REFER for the caller's participant.

agent.pypython
from livekit.agents import AgentSession, function_tool
from livekit.api import LiveKitAPI


@function_tool()
async def transfer_to_sales(context: RunContext):
  """Transfer the caller directly to the sales department."""

  await context.session.say("I'll transfer you to our sales team now. Goodbye!")

  api = LiveKitAPI()
  await api.sip.transfer_sip_participant(
      room_name=context.session.room.name,
      participant_identity="phone-caller",
      transfer_to="sip:+15559876543@trunk.provider.com",
  )
agent.tstypescript
import { AgentSession, functionTool } from "@livekit/agents";
import { LiveKitAPI } from "@livekit/server-sdk";

const transferToSales = functionTool({
name: "transfer_to_sales",
description: "Transfer the caller directly to the sales department.",
handler: async (params, context: AgentSession) => {
  await context.say("I'll transfer you to our sales team now. Goodbye!");

  const api = new LiveKitAPI();
  await api.sip.transferSipParticipant({
    roomName: context.room.name,
    participantIdentity: "phone-caller",
    transferTo: "sip:+15559876543@trunk.provider.com",
  });
},
});

Transferring to internal extensions

Cold transfers work equally well for internal extensions within a PBX system. Instead of a full E.164 phone number, you specify the extension in the SIP URI.

agent.pypython
from livekit.agents import RunContext, function_tool
from livekit.api import LiveKitAPI

EXTENSION_MAP = {
  "sales": "sip:100@pbx.company.com",
  "support": "sip:200@pbx.company.com",
  "billing": "sip:300@pbx.company.com",
  "voicemail": "sip:999@pbx.company.com",
}


@function_tool()
async def transfer_to_extension(
  context: RunContext,
  department: str,
):
  """Transfer the caller to a department extension.

  Args:
      department: The department to transfer to (sales, support, billing, voicemail).
  """
  target = EXTENSION_MAP.get(department)
  if not target:
      await context.session.say(
          f"I don't have an extension for {department}. "
          "Let me see how I can help you instead."
      )
      return

  await context.session.say(f"Transferring you to {department} now.")

  api = LiveKitAPI()
  await api.sip.transfer_sip_participant(
      room_name=context.session.room.name,
      participant_identity="phone-caller",
      transfer_to=target,
  )
agent.tstypescript
import { AgentSession, functionTool } from "@livekit/agents";
import { LiveKitAPI } from "@livekit/server-sdk";
import { z } from "zod";

const EXTENSION_MAP: Record<string, string> = {
sales: "sip:100@pbx.company.com",
support: "sip:200@pbx.company.com",
billing: "sip:300@pbx.company.com",
voicemail: "sip:999@pbx.company.com",
};

const transferToExtension = functionTool({
name: "transfer_to_extension",
description: "Transfer the caller to a department extension.",
parameters: z.object({
  department: z.string().describe("The department to transfer to"),
}),
handler: async ({ department }, context: AgentSession) => {
  const target = EXTENSION_MAP[department];
  if (!target) {
    await context.say(
      `I don't have an extension for ${department}. ` +
      "Let me see how I can help you instead."
    );
    return;
  }

  await context.say(`Transferring you to ${department} now.`);

  const api = new LiveKitAPI();
  await api.sip.transferSipParticipant({
    roomName: context.room.name,
    participantIdentity: "phone-caller",
    transferTo: target,
  });
},
});

Transferring to external numbers

When transferring to external phone numbers, format the SIP URI with the full E.164 number and your trunk provider's domain.

transfer_external.pypython
from livekit.api import LiveKitAPI


async def cold_transfer_to_number(
  room_name: str,
  participant_identity: str,
  phone_number: str,
  trunk_domain: str = "trunk.provider.com",
):
  """Cold transfer a caller to an external phone number.

  Args:
      room_name: The LiveKit room containing the call.
      participant_identity: The caller's participant identity.
      phone_number: E.164 formatted number (e.g., +15559876543).
      trunk_domain: Your SIP trunk provider's domain.
  """
  api = LiveKitAPI()
  await api.sip.transfer_sip_participant(
      room_name=room_name,
      participant_identity=participant_identity,
      transfer_to=f"sip:{phone_number}@{trunk_domain}",
  )
transferExternal.tstypescript
import { LiveKitAPI } from "@livekit/server-sdk";

async function coldTransferToNumber(
roomName: string,
participantIdentity: string,
phoneNumber: string,
trunkDomain: string = "trunk.provider.com"
): Promise<void> {
const api = new LiveKitAPI();
await api.sip.transferSipParticipant({
  roomName,
  participantIdentity,
  transferTo: `sip:${phoneNumber}@${trunkDomain}`,
});
}

E.164 format matters

Always use E.164 format for external numbers — including the country code with a + prefix. For example, +15559876543 for a US number. SIP trunks may reject or misroute numbers in other formats.

Handling REFER failures

A REFER can fail for several reasons: the target is unreachable, the trunk does not support REFER, or the receiving party rejects the call. Your system should detect failures and react.

agent.pypython
from livekit.agents import RunContext, function_tool
from livekit.api import LiveKitAPI


@function_tool()
async def safe_cold_transfer(context: RunContext, target: str):
  """Transfer with error handling.

  Args:
      target: SIP URI or phone number to transfer to.
  """
  await context.session.say("Transferring you now. One moment.")

  api = LiveKitAPI()
  try:
      await api.sip.transfer_sip_participant(
          room_name=context.session.room.name,
          participant_identity="phone-caller",
          transfer_to=target,
      )
  except Exception as e:
      # Transfer failed — stay on the line
      await context.session.say(
          "I'm sorry, I wasn't able to complete that transfer. "
          "I'm still here — how else can I help you?"
      )
agent.tstypescript
import { AgentSession, functionTool } from "@livekit/agents";
import { LiveKitAPI } from "@livekit/server-sdk";
import { z } from "zod";

const safeColdTransfer = functionTool({
name: "safe_cold_transfer",
description: "Transfer with error handling.",
parameters: z.object({
  target: z.string().describe("SIP URI or phone number to transfer to"),
}),
handler: async ({ target }, context: AgentSession) => {
  await context.say("Transferring you now. One moment.");

  const api = new LiveKitAPI();
  try {
    await api.sip.transferSipParticipant({
      roomName: context.room.name,
      participantIdentity: "phone-caller",
      transferTo: target,
    });
  } catch (error) {
    // Transfer failed — stay on the line
    await context.say(
      "I'm sorry, I wasn't able to complete that transfer. " +
      "I'm still here — how else can I help you?"
    );
  }
},
});

Choosing between warm and cold

FactorWarm transferCold transfer
Caller contextPreserved — receiving agent is briefedLost — caller may need to repeat
SpeedSlower (dial + brief + connect)Fast (immediate redirect)
Caller experienceBest for complex issuesFine for simple routing
InfrastructureRequires outbound trunk + room managementOnly requires REFER support
Failure impactCaller returns to AI agentCaller may be disconnected
What's happening

In practice, implement both transfer types and let the AI agent's judgment (or explicit caller request) determine which to use. When the caller says "just transfer me to sales," use a cold transfer. When the caller has spent five minutes explaining a complex insurance claim, use a warm transfer.

Test your knowledge

Question 1 of 2

What is the fundamental tradeoff between cold transfers and warm transfers?

What you learned

  • Cold transfers use SIP REFER to redirect calls immediately without briefing the receiving party.
  • The SIP URI format determines whether you are transferring to an external number or an internal extension.
  • REFER support varies by SIP trunk provider — verify compatibility before deployment.
  • Transfer failures must be caught so the caller is not left in silence.
  • The choice between warm and cold transfers depends on context, complexity, and caller preference.

Next up

In the next chapter, you will set up call recording and compliance — ensuring every call is captured, stored securely, and managed according to regulatory requirements.

Concepts covered
Cold transferSIP REFERTransfer targets