Skip to main content

Twilio Connector

Connect LiveKit to Twilio phone calls using WebSocket connections.

Available in
Beta

Overview

The Twilio Connector uses Twilio 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.

Note

There are several ways to connect Twilio calls to LiveKit. Elastic SIP Trunking 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:

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 connector participant in the LiveKit room.

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' }],
});
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")],
)
)
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",
},
},
})
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()
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 :

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

<?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  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  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:

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.

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 .
  • 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 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:

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>`;
}
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>"""
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))
}
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>"""
}
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 .
  2. In the side navigation, select the Develop tab, then select Phone NumbersManageActive 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:

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"}',
},
],
});
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"}',
),
],
)
)
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"}`,
},
},
})
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()
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.

Monitoring calls

Monitor Twilio call status using LiveKit's participant 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:

room.on('participantConnected', (participant) => {
console.log(`Twilio participant connected: ${participant.identity}`);
});
room.on('participantDisconnected', (participant) => {
console.log(`Twilio participant disconnected: ${participant.identity}`);
});
@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}")
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())
})
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}")
}
}
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