LiveKit docs › Features › Transfers › Call forwarding

---

# Call forwarding

> Transfer calls to another number or SIP endpoint using SIP REFER.

A _cold transfer_ refers to forwarding a caller to another phone number or SIP endpoint. Performing a cold transfer closes the caller's LiveKit session.

For transfers that include an AI agent to provide context, see the [Agent-assisted transfer](https://docs.livekit.io/telephony/features/transfers/warm.md) guide.

## How it works

To transfer a caller out of a LiveKit room to another phone number, use the following steps:

1. Call the `TransferSIPParticipant` API.
2. LiveKit sends a SIP REFER through your trunk, instructing the provider to connect the caller to the new number or SIP endpoint.
3. The caller leaves the LiveKit room, ending the session.

## Transferring a SIP participant using SIP REFER

REFER is a SIP method that allows you to move an active session to another endpoint (that is, transfer a call). For LiveKit telephony apps, you can use the [`TransferSIPParticipant`](https://docs.livekit.io/reference/telephony/sip-api.md#transfersipparticipant) server API to transfer a caller to another phone number or SIP endpoint.

In order to successfully transfer calls, you must configure your provider trunks to allow call transfers.

### Enable call transfers for your Twilio SIP trunk

Enable call transfer and PSTN transfers for your Twilio SIP trunk. To learn more, see Twilio's [Call Transfer via SIP REFER](https://www.twilio.com/docs/sip-trunking/call-transfer) documentation.

When you transfer a call, you have the option to set the caller ID to display the phone number of the transferee (the caller) or the transferrer (the phone number associated with your LiveKit trunk). Caller ID is configured on the trunk and can't be set per-transfer through the `TransferSIPParticipant` API.

**CLI**:

The following command enables call transfers and sets the caller ID to display the number of the transferee:

> ℹ️ **Note**
> 
> - To list trunks, execute `twilio api trunking v1 trunks list`.
> - To set the caller ID to the transferor, set `transfer-caller-id` to `from-transferor`.

```shell
twilio api trunking v1 trunks update --sid <twilio-trunk-sid> \
--transfer-mode enable-all \
--transfer-caller-id from-transferee

```

---

**Console**:

1. Sign in to the [Twilio console](https://console.twilio.com).
2. Navigate to **Elastic SIP Trunking** » **Manage** » **Trunks**, and select a trunk.
3. In the **Features** » **Call Transfer (SIP REFER)** section, select **Enabled**.
4. In the **Caller ID for Transfer Target** field, select an option.
5. Select **Enable PSTN Transfer**.
6. Save your changes.

### Usage

Set up the following environment variables:

```shell
export LIVEKIT_URL=%{wsURL}%
export LIVEKIT_API_KEY=<YOUR_API_KEY>
export LIVEKIT_API_SECRET=<YOUR_API_SECRET>

```

**Node.js**:

This example uses the LiveKit URL, API key, and secret set as environment variables.

```typescript
import { SipClient } from 'livekit-server-sdk';

// ...

async function transferParticipant(participant) {
  console.log("transfer participant initiated");

  const sipTransferOptions = {
    playDialtone: false
  };

  const sipClient = new SipClient(process.env.LIVEKIT_URL,
                                  process.env.LIVEKIT_API_KEY,
                                  process.env.LIVEKIT_API_SECRET);

  const transferTo = "tel:+15105550100";

  try {
    await sipClient.transferSipParticipant('open-room', participant.identity, transferTo, sipTransferOptions);
    console.log("SIP participant transferred successfully");
  } catch (error) {
    if (error instanceof TwirpError && error.metadata != null) {
      console.error("SIP error code: ", error.metadata?.['sip_status_code']);
      console.error("SIP error message: ", error.metadata?.['sip_status']);
    } else {
      console.error("Error transferring SIP participant: ", error);
    }
  }
}

```

---

**Python**:

```python
import asyncio
import logging
import os

from livekit import api
from livekit.protocol.sip import TransferSIPParticipantRequest

logger = logging.getLogger("transfer-logger")
logger.setLevel(logging.INFO)

async def transfer_call(participant_identity: str, room_name: str) -> None:
  async with api.LiveKitAPI() as livekit_api:
    transfer_to = 'tel:+14155550100'

    try:
      # Create transfer request
      transfer_request = TransferSIPParticipantRequest(
          participant_identity=participant_identity,
          room_name=room_name,
          transfer_to=transfer_to,
          play_dialtone=False
      )
      logger.debug(f"Transfer request: {transfer_request}")
          
      # Transfer caller
      await livekit_api.sip.transfer_sip_participant(transfer_request)
      print("SIP participant transferred successfully")
          
    except Exception as error:
        # Check if it's a Twirp error with metadata
        if hasattr(error, 'metadata') and error.metadata:
            print(f"SIP error code: {error.metadata.get('sip_status_code')}")
            print(f"SIP error message: {error.metadata.get('sip_status')}")
        else:
            print(f"Error transferring SIP participant:")
            print(f"{error.status} - {error.code} - {error.message}")

```

For a full example using a voice agent, DTMF, and SIP REFER, see the [phone assistant example](https://github.com/ShayneP/phone-assistant).

---

**Ruby**:

```ruby
require 'livekit'

room_name = 'open-room'
participant_identity = 'participant_identity'

def transferParticipant(room_name, participant_identity)

  sip_service = LiveKit::SIPServiceClient.new(
    ENV['LIVEKIT_URL'],
    api_key: ENV['LIVEKIT_API_KEY'],
    api_secret: ENV['LIVEKIT_API_SECRET']
  )

  transfer_to = 'tel:+14155550100'

    response = sip_service.transfer_sip_participant(
        room_name,
        participant_identity,
        transfer_to,
        play_dialtone: false
    )

     if response.error then
        puts "Error: #{response.error}"
    else
        puts "SIP participant transferred successfully"
    end

end

```

---

**Go**:

```go
import (
	"context"
	"fmt"
	"os"

	"github.com/livekit/protocol/livekit"
	lksdk "github.com/livekit/server-sdk-go/v2"
)

func transferParticipant(ctx context.Context, participantIdentity string) {
	fmt.Println("Starting SIP participant transfer...")

	roomName := "open-room"
	transferTo := "tel:+14155550100"

	// Create a transfer request
	transferRequest := &livekit.TransferSIPParticipantRequest{
		RoomName:            roomName,
		ParticipantIdentity: participantIdentity,
		TransferTo:          transferTo,
		PlayDialtone:        false,
	}

	fmt.Println("Creating SIP client...")
	sipClient := lksdk.NewSIPClient(os.Getenv("LIVEKIT_URL"),
		os.Getenv("LIVEKIT_API_KEY"),
		os.Getenv("LIVEKIT_API_SECRET"))

	// Execute transfer request
	fmt.Println("Executing transfer request...")
	_, err := sipClient.TransferSIPParticipant(ctx, transferRequest)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("SIP participant transferred successfully")
}

```

---

**CLI**:

```shell
lk sip participant transfer --room <CURRENT_ROOM> \
   --identity <PARTICIPANT_ID> \
  --to "<SIP_ENDPOINT>"

```

Where `<SIP_ENDPOINT>` is a valid SIP endpoint or telephone number. The following examples are valid formats:

- `tel:+15105550100`
- `sip:+15105550100@sip.telnyx.com`
- `sip:+15105550100@my-livekit-demo.pstn.twilio.com`

## Forward calls with an agent tool

Your agent can use the `TransferSIPParticipant` API to transfer calls without staying on the line. The current session ends after the transfer is complete. The following example shows how to define a tool in your agent class that calls `TransferSIPParticipant`.

`TransferSIPParticipant` requires the `participant_identity` of the SIP caller in the room, which is assigned at dispatch time and might differ from the caller's phone number. To reliably find the active SIP caller, look up the participant in the `remote_participants` list and filter on `ParticipantKind.SIP`. To learn more, see [Identifying SIP callers](https://docs.livekit.io/telephony/accepting-calls/workflow-setup.md#identifying-sip-callers).

The following examples assume a single SIP caller per room, which is the typical inbound-agent setup. If your room can contain multiple SIP participants (for example, during a warm transfer or conference), track the target caller's identity explicitly instead of picking the first SIP participant.

**Python**:

```python
from livekit import api, rtc
from livekit.agents import Agent, RunContext, function_tool, get_job_context

class Assistant(Agent):
    ## ... existing init code ...

    @function_tool()
    async def transfer_call(self, ctx: RunContext):
        """Transfer the call to a human agent, called after confirming with the user"""

        transfer_to = "+15105550123"

        job_ctx = get_job_context()

        # Find the active SIP caller in the room. The identity is set at
        # dispatch time and might not match the caller's phone number.
        # Assumes a single SIP caller per room.
        sip_participant = next(
            (
                p for p in job_ctx.room.remote_participants.values()
                if p.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP
            ),
            None,
        )
        if sip_participant is None:
            return "no active SIP caller to transfer"

        # let the message play fully before transferring
        await ctx.session.generate_reply(
            instructions="Inform the user that you're transferring them to a different agent."
        )

        try:
            await job_ctx.api.sip.transfer_sip_participant(
                api.TransferSIPParticipantRequest(
                    room_name=job_ctx.room.name,
                    participant_identity=sip_participant.identity,
                    # to use a sip destination, use `sip:user@host` format
                    transfer_to=f"tel:{transfer_to}",
                )
            )
        except Exception as e:
            print(f"error transferring call: {e}")
            # give the LLM that context
            return "could not transfer call"

```

---

**Node.js**:

To use the Node.js example, install the `livekit-server-sdk` package:

```shell
pnpm add livekit-server-sdk

```

Define the transfer tool on your agent class using `llm.tool`. The following example shows a complete `Agent` with a `transferCall` tool. Replace the `src/agent.ts` file in the [`agent-starter-node`](https://github.com/livekit-examples/agent-starter-node) project with the following code:

```typescript
import { voice, llm, getJobContext } from '@livekit/agents';
import { SipClient } from 'livekit-server-sdk';
import { ParticipantKind } from '@livekit/rtc-node';
import { z } from 'zod';

export class Agent extends voice.Agent {
  constructor() {
    super({
      instructions: 'You are a helpful assistant.',
      tools: {
        transferCall: llm.tool({
          description:
            'Transfer the call to a human agent, called after confirming with the user.',
          parameters: z.object({}),
          execute: async (_, { ctx }) => {
            const transferTo = 'tel:+15105550123';
            const jobCtx = getJobContext();
            const room = jobCtx.room;

            // Find the active SIP caller in the room. The identity is set at
            // dispatch time and might not match the caller's phone number.
            // Assumes a single SIP caller per room.
            const sipParticipant = Array.from(room.remoteParticipants.values()).find(
              (p) => p.kind === ParticipantKind.SIP,
            );
            if (!sipParticipant) {
              return 'no active SIP caller to transfer';
            }

            // Let the message play fully before transferring
            ctx.session.generateReply({
              instructions: "Inform the user that you're transferring them to a different agent.",
            });
            await ctx.waitForPlayout();

            const sipClient = new SipClient(
              process.env.LIVEKIT_URL!,
              process.env.LIVEKIT_API_KEY!,
              process.env.LIVEKIT_API_SECRET!,
            );

            try {
              await sipClient.transferSipParticipant(
                room.name!,
                sipParticipant.identity,
                transferTo,
                { playDialtone: false },
              );
            } catch (e) {
              console.log(`error transferring call: ${e}`);
              return 'could not transfer call';
            }
          },
        }),
      },
    });
  }
}

```

## Additional resources

The following guides provide more information on building voice agents for telephony.

- **[Agent-assisted warm transfer](https://docs.livekit.io/telephony/features/transfers/warm.md)**: Transfer calls with agent assistance and context.

- **[Tool definition & use](https://docs.livekit.io/agents/build/tools.md)**: Extend your agent's capabilities with tools.

- **[Workflows](https://docs.livekit.io/agents/logic/workflows.md)**: Orchestrate detailed workflows such as collecting credit card information over the phone.

- **[Agent speech](https://docs.livekit.io/agents/build/audio.md)**: Customize and perfect your agent's verbal interactions.

---

This document was rendered at 2026-06-07T11:35:49.473Z.
For the latest version of this document, see [https://docs.livekit.io/telephony/features/transfers/cold.md](https://docs.livekit.io/telephony/features/transfers/cold.md).

To explore all LiveKit documentation, see [llms.txt](https://docs.livekit.io/llms.txt).