LiveKit docs › Connectors › Twilio

---

# Twilio Connector

> Connect LiveKit to Twilio phone calls using WebSocket connections.

Available in (BETA):
- [ ] Node.js
- [ ] Python

## Overview

The Twilio Connector uses [Twilio Media Streams](https://www.twilio.com/docs/voice/media-streams#bidirectional-media-streams) to connect phone calls to LiveKit rooms over WebSockets instead of SIP. You can use it to connect inbound and outbound phone calls to LiveKit rooms. When combined with LiveKit Agents, you can deploy AI voice agents to handle phone calls.

For each Twilio call, the connector creates a dedicated LiveKit participant—referred to as the _connector participant_ in this topic—which communicates with other participants in the room. You can identify these participants by their `kind` field which is set to `CONNECTOR`. To learn more, see [Types of participants](https://docs.livekit.io/intro/basics/rooms-participants-tracks/participants.md#types-of-participants).

> ℹ️ **Note**
> 
> There are several ways to connect Twilio calls to LiveKit. [Elastic SIP Trunking](https://docs.livekit.io/telephony/start/providers/twilio.md) is the recommended approach because it provides the most flexibility and feature support. Use the Twilio Connector if you already have Twilio workflows and want to integrate LiveKit with minimal changes.

## Prerequisites

To use the Twilio Connector, you need the following:

- [Twilio Account](https://console.twilio.com): Required to access Twilio's programmable voice APIs.
- [Twilio Phone Number](https://www.twilio.com/console/phone-numbers/search): A verified phone number registered with Twilio.
- [Twilio Credentials](https://www.twilio.com/console/project/settings): Account SID and Auth Token from your Twilio console.
- [LiveKit Project](https://cloud.livekit.io/projects/p_/settings/project): Either LiveKit Cloud or a self-hosted LiveKit server.

## Making outbound calls

The following sections outline the workflow for making outbound calls.

### Step 1: Create a connector session

Call the `ConnectTwilioCall` API to create a connector session. This API generates a WebSocket URL that Twilio uses to stream audio to and from LiveKit. It also creates a [hidden](https://docs.livekit.io/intro/basics/rooms-participants-tracks/participants.md#hidden-participants) connector participant in the LiveKit room.

**Node.js**:

```typescript
import { ConnectorClient } from 'livekit-server-sdk';
import { ConnectTwilioCallRequest_TwilioCallDirection } from '@livekit/protocol';

const connectorClient = new ConnectorClient(
  process.env.LIVEKIT_URL,
  process.env.LIVEKIT_API_KEY,
  process.env.LIVEKIT_API_SECRET,
);

const res = await connectorClient.connectTwilioCall({
  twilioCallDirection: ConnectTwilioCallRequest_TwilioCallDirection.TWILIO_CALL_DIRECTION_OUTBOUND,
  roomName: 'twilio-connector-test',
  destinationCountry: 'US', // optional
  participantIdentity: 'test', // optional
  participantName: 'test', // optional
  agents: [{ agentName: 'my-agent' }],
});

```

---

**Python**:

```python
from livekit import api

from livekit.protocol.agent_dispatch import RoomAgentDispatch

lkapi = api.LiveKitAPI()
res = await lkapi.connector.connect_twilio_call(
    api.ConnectTwilioCallRequest(
        twilio_call_direction=api.ConnectTwilioCallRequest.TWILIO_CALL_DIRECTION_OUTBOUND,
        destination_country="US",  # optional
        room_name="twilio-connector-test",
        participant_identity="test",  # optional
        participant_name="test",  # optional
        agents=[RoomAgentDispatch(agent_name="my-agent")],
    )
)

```

---

**Go**:

```go
res, err := connectorClient.ConnectTwilioCall(ctx, &livekit.ConnectTwilioCallRequest{
    TwilioCallDirection: livekit.ConnectTwilioCallRequest_TWILIO_CALL_DIRECTION_OUTBOUND,
    DestinationCountry:  "US",                 // optional
    RoomName:            "twilio-connector-test",
    ParticipantIdentity: "test",               // optional
    ParticipantName:     "test",               // optional
    Agents: []*livekit.RoomAgentDispatch{
        {
            AgentName: "my-agent",
        },
    },
})

```

---

**Kotlin**:

```kotlin
import io.livekit.server.ConnectorServiceClient
import io.livekit.server.TwilioCallOptions
import livekit.LivekitConnectorTwilio.ConnectTwilioCallRequest.TwilioCallDirection

val connectorClient = ConnectorServiceClient.createClient(
  host = System.getenv("LIVEKIT_URL").replaceFirst(Regex("^ws"), "http"),
  apiKey = System.getenv("LIVEKIT_API_KEY"),
  secret = System.getenv("LIVEKIT_API_SECRET"),
)

import livekit.LivekitAgentDispatch.RoomAgentDispatch

val response = connectorClient.connectTwilioCall(
    twilioCallDirection = TwilioCallDirection.TWILIO_CALL_DIRECTION_OUTBOUND,
    options = TwilioCallOptions(
        roomName = "twilio-connector-test",
        destinationCountry = "US", // optional
        participantIdentity = "test", // optional
        participantName = "test", // optional
        agents = listOf(
            RoomAgentDispatch.newBuilder()
                .setAgentName("my-agent")
                .build(),
        ),
    ),
).execute()

```

---

**Rust**:

```rust
use livekit_api::services::connector::{ConnectorClient, ConnectTwilioCallOptions};
use livekit_protocol::connect_twilio_call_request::TwilioCallDirection;

let connector_client = ConnectorClient::with_api_key(host, api_key, api_secret);

use livekit_protocol::RoomAgentDispatch;

let res = connector_client
    .connect_twilio_call(
        TwilioCallDirection::Outbound,
        "twilio-connector-test",
        ConnectTwilioCallOptions {
            destination_country: Some("US".into()), // optional
            participant_identity: Some("test".into()), // optional
            participant_name: Some("test".into()), // optional
            agents: Some(vec![RoomAgentDispatch {
                agent_name: "my-agent".into(),
                ..Default::default()
            }]),
            ..Default::default()
        },
    )
    .await?;

```

The response contains the following:

`connect_url`: A WebSocket URL used by Twilio MediaStreams.

### Step 2: Establish bidirectional media stream

Use a webhook to generate Twilio Markup Language (TwiML) using the `connect_url` returned in the previous step. This TwiML response establishes the bidirectional media stream using the API in the [Twilio WebSocket message API](https://www.twilio.com/docs/voice/media-streams/websocket-messages):

The TwiML response should look like the following example. Replace `<connect_url>` with the actual WebSocket URL returned from the `ConnectTwilioCall` API:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
    <Stream url="<connect_url>" />
</Connect>
<Say>This TwiML instruction is unreachable unless the Stream is ended by your WebSocket server.</Say>
</Response> 

```

### Step 3: Call the user

Call the user using the Twilio [create call](https://www.twilio.com/docs/voice/api/call-resource#create-a-call) API with the `url` parameter. The `<twiml_url>` in the following example is the HTTPS version of the `connect_url`. Simply replace `wss` with `https` in the `connect_url`.

Twilio offers a [CLI command](https://www.twilio.com/docs/twilio-cli/quickstart) to create a call. The `<from_phone>` is the phone number you purchased from Twilio, and the `<to_phone>` is the user you want to call:

```shell
twilio api:core:calls:create --from "<from_phone>" --to "<to_phone>" --url <twiml_url>

```

### Step 4: User accepts or declines the call

After the user answers the phone and hears **"This is LiveKit Twilio Connector"**, Twilio connects to the LiveKit connector service through the WebSocket URL returned from the `ConnectTwilioCall` API in the previous step, and the call is bridged into the LiveKit room. At this point, the connector participant becomes visible to other participants in the room.

When the WebSocket connection ends, the connector participant automatically leaves the room. If the connector service does not receive a WebSocket request before the default timeout (for example, the call failed or the user denied the call), it ends the connector session and leaves the LiveKit room.

### ConnectTwilioCall API parameters

The following parameters are required to connect a Twilio call:

- `twilio_call_direction`: Indicates the direction of the call. For outbound calls, the connector participant immediately joins the room as a _hidden_ participant, and becomes visible to other participants only after the Twilio call is accepted.
- `room_name`: The LiveKit room to place the participant in.

You can also specify the optional parameters to specify agent dispatch, and participant's identity, name, metadata, and attributes. For a full list of parameters see [ConnectTwilioCall](https://docs.livekit.io/reference/telephony/connectors-api.md#connecttwiliocall).

## Accepting inbound calls

The following sections outline the workflow for accepting incoming calls. Your Twilio phone number must be configured to use a webhook endpoint that returns TwiML containing the WebSocket connection details.

### Step 1: Implement webhook endpoint

Your webhook endpoint should do the following:

- Accept POST requests with Twilio call [request parameters](https://www.twilio.com/docs/voice/twiml#request-parameters).
- Call `ConnectTwilioCall` to get the WebSocket URL.
- Return TwiML with the `<Stream>` element that points to the WebSocket URL returned by `ConnectTwilioCall`.

#### TwiML example

The TwiML response should look like the following example. Replace `<connect_url>` with the actual WebSocket URL returned from the `ConnectTwilioCall` API:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>Connecting you to support.</Say>
    <Connect>
        <Stream url="<connect_url>" />
    </Connect>
</Response>

```

#### Example webhook handler

The following example webhook handler parses the Twilio webhook parameters and calls the `ConnectTwilioCall` API to get the WebSocket URL. It then returns TwiML with the `<Stream>` element that points to the WebSocket URL:

**Node.js**:

```typescript
import { ConnectorClient } from 'livekit-server-sdk';
import { ConnectTwilioCallRequest_TwilioCallDirection } from '@livekit/protocol';

const connectorClient = new ConnectorClient(
  process.env.LIVEKIT_URL,
  process.env.LIVEKIT_API_KEY,
  process.env.LIVEKIT_API_SECRET,
);

// In your webhook handler
async function handleIncomingCall(from: string, callSid: string): Promise<string> {
  const response = await connectorClient.connectTwilioCall({
    twilioCallDirection: ConnectTwilioCallRequest_TwilioCallDirection.TWILIO_CALL_DIRECTION_INBOUND,
    roomName: `call-${callSid}`,
    participantIdentity: from,
    participantName: from,
    agents: [{ agentName: 'support-agent' }],
  });

  // Return TwiML with the WebSocket URL
  return `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>This is LiveKit Twilio Connector.</Say>
    <Connect>
        <Stream url="${response.connectUrl}" />
    </Connect>
</Response>`;
}

```

---

**Python**:

```python
from livekit import api
from livekit.protocol.agent_dispatch import RoomAgentDispatch

lkapi = api.LiveKitAPI()

# In your webhook handler
async def handle_incoming_call(from_number: str, call_sid: str) -> str:
    response = await lkapi.connector.connect_twilio_call(
        api.ConnectTwilioCallRequest(
            twilio_call_direction=api.ConnectTwilioCallRequest.TWILIO_CALL_DIRECTION_INBOUND,
            room_name=f"call-{call_sid}",
            participant_identity=from_number,
            participant_name=from_number,
            agents=[RoomAgentDispatch(agent_name="support-agent")],
        )
    )

    # Return TwiML with the WebSocket URL
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>This is LiveKit Twilio Connector.</Say>
    <Connect>
        <Stream url="{response.connect_url}" />
    </Connect>
</Response>"""

```

---

**Go**:

```go
func handleIncomingCall(w http.ResponseWriter, r *http.Request) {
    // Parse Twilio webhook parameters
    from := r.FormValue("From")
    callSid := r.FormValue("CallSid")

    // Connect to LiveKit
    response, err := connectorClient.ConnectTwilioCall(context.Background(), &livekit.ConnectTwilioCallRequest{
        TwilioCallDirection: livekit.ConnectTwilioCallRequest_TWILIO_CALL_DIRECTION_INBOUND,
        RoomName:            fmt.Sprintf("call-%s", callSid),
        ParticipantIdentity: from,
        ParticipantName:     from,
        Agents: []*livekit.RoomAgentDispatch{
            {
                AgentName: "support-agent",
            },
        },
    })

    if err != nil {
        http.Error(w, "Failed to connect call", http.StatusInternalServerError)
        return
    }

    // Return TwiML with the WebSocket URL
    twiml := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>This is LiveKit Twilio Connector.</Say>
    <Connect>
        <Stream url="%s" />
    </Connect>
</Response>`, response.ConnectUrl)

    w.Header().Set("Content-Type", "application/xml")
    w.Write([]byte(twiml))
}

```

---

**Kotlin**:

```kotlin
import io.livekit.server.ConnectorServiceClient
import io.livekit.server.TwilioCallOptions
import livekit.LivekitAgentDispatch.RoomAgentDispatch
import livekit.LivekitConnectorTwilio.ConnectTwilioCallRequest.TwilioCallDirection

// In your webhook handler
fun handleIncomingCall(from: String, callSid: String): String {
    val response = connectorClient.connectTwilioCall(
        twilioCallDirection = TwilioCallDirection.TWILIO_CALL_DIRECTION_INBOUND,
        options = TwilioCallOptions(
            roomName = "call-$callSid",
            participantIdentity = from,
            participantName = from,
            agents = listOf(
                RoomAgentDispatch.newBuilder()
                    .setAgentName("support-agent")
                    .build(),
            ),
        ),
    ).execute().body()!!

    // Return TwiML with the WebSocket URL
    return """<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>This is LiveKit Twilio Connector.</Say>
    <Connect>
        <Stream url="${response.connectUrl}" />
    </Connect>
</Response>"""
}

```

---

**Rust**:

```rust
use livekit_api::services::connector::{ConnectorClient, ConnectTwilioCallOptions};
use livekit_protocol::{connect_twilio_call_request::TwilioCallDirection, RoomAgentDispatch};

// In your webhook handler
async fn handle_incoming_call(
    connector_client: &ConnectorClient,
    from: &str,
    call_sid: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    let response = connector_client
        .connect_twilio_call(
            TwilioCallDirection::Inbound,
            format!("call-{call_sid}"),
            ConnectTwilioCallOptions {
                participant_identity: Some(from.into()),
                participant_name: Some(from.into()),
                agents: Some(vec![RoomAgentDispatch {
                    agent_name: "support-agent".into(),
                    ..Default::default()
                }]),
                ..Default::default()
            },
        )
        .await?;

    // Return TwiML with the WebSocket URL
    Ok(format!(
        r#"<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>This is LiveKit Twilio Connector.</Say>
    <Connect>
        <Stream url="{}" />
    </Connect>
</Response>"#,
        response.connect_url
    ))
}

```

### Step 2: Configure Twilio phone number

To handle incoming Twilio calls, configure your Twilio phone number with a webhook URL that returns TwiML containing the WebSocket connection details.

1. Sign in to the [Twilio Console](https://console.twilio.com).
2. In the side navigation, select the **Develop** tab, then select **Phone Numbers** → **Manage** → **Active Numbers**.
3. Select the phone number you want to configure.
4. In the **Voice Configuration** section → **Configure with**, select **Webhook, TwiML Bin, Function, Studio Flow, Proxy Service**.
5. For **A call comes in**, select **Webhook**.
6. For **URL**, enter the URL of your webhook endpoint from the previous step.
7. For **HTTP**, select **HTTP POST**.
8. Select **Save configuration**.

### Step 3: Call is connected to LiveKit

After a user dials your number and the webhook returns the TwiML response, a bidirectional media stream is established between LiveKit and Twilio. At this point a participant is created in the LiveKit room with the identity and name specified in the `ConnectTwilioCall` API. Unlike for outbound calls, the participant is created as a _visible_ participant. The Twilio call is now bridged into the LiveKit room.

## Agent dispatch

You can automatically dispatch LiveKit Agents to Twilio calls by including agent dispatch rules in your call requests. This enables AI voice agents to interact with callers.

### Example with agent dispatch

The following example code shows how to connect a Twilio call to a LiveKit room and dispatch an agent named `customer-support-bot` to the call:

**Node.js**:

```typescript
const response = await connectorClient.connectTwilioCall({
  twilioCallDirection: ConnectTwilioCallRequest_TwilioCallDirection.TWILIO_CALL_DIRECTION_OUTBOUND,
  roomName: 'support-room',
  participantIdentity: 'caller-123',
  agents: [
    {
      agentName: 'customer-support-bot',
      metadata: '{"language": "en", "queue": "technical"}',
    },
  ],
});

```

---

**Python**:

```python
from livekit.protocol.agent_dispatch import RoomAgentDispatch

response = await lkapi.connector.connect_twilio_call(
    api.ConnectTwilioCallRequest(
        twilio_call_direction=api.ConnectTwilioCallRequest.TWILIO_CALL_DIRECTION_OUTBOUND,
        room_name="support-room",
        participant_identity="caller-123",
        agents=[
            RoomAgentDispatch(
                agent_name="customer-support-bot",
                metadata='{"language": "en", "queue": "technical"}',
            ),
        ],
    )
)

```

---

**Go**:

```go
response, err := connectorClient.ConnectTwilioCall(context.Background(), &livekit.ConnectTwilioCallRequest{
    TwilioCallDirection: livekit.ConnectTwilioCallRequest_TWILIO_CALL_DIRECTION_OUTBOUND,
    RoomName:            "support-room",
    ParticipantIdentity: "caller-123",
    Agents: []*livekit.RoomAgentDispatch{
        {
            AgentName: "customer-support-bot",
            Metadata:  `{"language": "en", "queue": "technical"}`,
        },
    },
})

```

---

**Kotlin**:

```kotlin
import livekit.LivekitAgentDispatch.RoomAgentDispatch
import livekit.LivekitConnectorTwilio.ConnectTwilioCallRequest.TwilioCallDirection

val response = connectorClient.connectTwilioCall(
    twilioCallDirection = TwilioCallDirection.TWILIO_CALL_DIRECTION_OUTBOUND,
    options = TwilioCallOptions(
        roomName = "support-room",
        participantIdentity = "caller-123",
        agents = listOf(
            RoomAgentDispatch.newBuilder()
                .setAgentName("customer-support-bot")
                .setMetadata("""{"language": "en", "queue": "technical"}""")
                .build(),
        ),
    ),
).execute()

```

---

**Rust**:

```rust
use livekit_protocol::RoomAgentDispatch;

let response = connector_client
    .connect_twilio_call(
        TwilioCallDirection::Outbound,
        "support-room",
        ConnectTwilioCallOptions {
            participant_identity: Some("caller-123".into()),
            agents: Some(vec![RoomAgentDispatch {
                agent_name: "customer-support-bot".into(),
                metadata: r#"{"language": "en", "queue": "technical"}"#.into(),
            }]),
            ..Default::default()
        },
    )
    .await?;

```

For more information on creating voice agents, see the [LiveKit Agents documentation](https://docs.livekit.io/agents/overview.md).

## Monitoring calls

Monitor Twilio call status using LiveKit's participant [events](https://docs.livekit.io/intro/basics/rooms-participants-tracks/webhooks-events.md#sdk-events):

- Listen for participant connection events to know when a call is established.
- Listen for participant disconnection events to know when a call ends.
- Use participant attributes to track call-specific metadata.

### Example

The following example logs the identity of the participant when they connect or disconnect:

**Node.js**:

```typescript
room.on('participantConnected', (participant) => {
  console.log(`Twilio participant connected: ${participant.identity}`);
});

room.on('participantDisconnected', (participant) => {
  console.log(`Twilio participant disconnected: ${participant.identity}`);
});

```

---

**Python**:

```python
@room.on("participant_connected")
def on_participant_connected(participant):
    print(f"Twilio participant connected: {participant.identity}")

@room.on("participant_disconnected")
def on_participant_disconnected(participant):
    print(f"Twilio participant disconnected: {participant.identity}")

```

---

**Go**:

```go
room.Callback.OnParticipantConnected(func(participant *lksdk.RemoteParticipant) {
    log.Printf("Twilio participant connected: %s", participant.Identity())
})

room.Callback.OnParticipantDisconnected(func(participant *lksdk.RemoteParticipant) {
    log.Printf("Twilio participant disconnected: %s", participant.Identity())
})

```

---

**Kotlin**:

```kotlin
room.listener = object : RoomListener {
    override fun onParticipantConnected(room: Room, participant: RemoteParticipant) {
        println("Twilio participant connected: ${participant.identity}")
    }

    override fun onParticipantDisconnected(room: Room, participant: RemoteParticipant) {
        println("Twilio participant disconnected: ${participant.identity}")
    }
}

```

---

**Rust**:

```rust
room.on_participant_connected(|participant| {
    println!("Twilio participant connected: {}", participant.identity());
});

room.on_participant_disconnected(|participant| {
    println!("Twilio participant disconnected: {}", participant.identity());
});

```

## Troubleshooting

The following troubleshooting steps can help you resolve common issues with the Twilio Connector.

### Call not connecting

- Verify your Twilio credentials are correct.
- Ensure your webhook URL is publicly accessible from Twilio's servers.
- Check that the WebSocket URL is properly included in your TwiML response.
- Verify your LiveKit server is running and accessible.

### Audio quality issues

- Check network connectivity between LiveKit and Twilio's infrastructure.
- Verify media tracks are being published correctly in the LiveKit room.
- Ensure your LiveKit server has sufficient resources to handle media processing.

### TwiML errors

- Verify your TwiML is well-formed XML.
- Ensure the WebSocket URL uses the `wss://` protocol.
- Check Twilio debugger logs in your Twilio Console for detailed error messages.

### Webhook not receiving requests

- Verify webhook URL is correct in Twilio Console.
- Ensure webhook endpoint is publicly accessible.
- Check that your server is returning HTTP 200 responses.
- Verify webhook signature validation if enabled.

## Next steps

- **[LiveKit Agents](https://docs.livekit.io/agents/overview.md)**: Build AI voice agents to handle Twilio calls.

- **[Participant APIs](https://docs.livekit.io/home/server/managing-participants.md)**: Learn how to manage participants in LiveKit rooms.

---

This document was rendered at 2026-06-07T11:36:31.740Z.
For the latest version of this document, see [https://docs.livekit.io/telephony/connectors/twilio.md](https://docs.livekit.io/telephony/connectors/twilio.md).

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