Skip to main content
This is the sip transport with the twilio carrier. Your number is attached to a Twilio Elastic SIP Trunk that points at LiveKit SIP. LiveKit accepts the call because a record in your project claims that number, a second record dispatches it to your agent, and the agent answers in a room of its own.
This route is SIP. It shares nothing with the Pipecat Twilio route except the account the number lives in. It is also the route with the most capability: cold transfer, warm transfer, voicemail detection, and seven of the eight call facts a pre-fetch can read before the greeting, every one but stream_id. The two Pipecat Twilio routes grant a smaller set; see Where it works for the full grid.
Your own build/livekit/README.md is the authority for your build. It names your agent, your number’s variable, and the two commands below with your values filled in. This page is the same route with the reasons attached.
On this page:

Two things are called a trunk

The setup below creates one trunk in Twilio and one in LiveKit, and they do different jobs: Finishing the Twilio side is half the job. LiveKit rejects a call whose number no inbound trunk claims, so the generated script has to run too. The Twilio trunk itself has two sides, on two tabs that sound similar and do opposite things: An inbound-only agent still declares all four SIP names, because the route requires them whichever direction it uses, and a cold transfer needs the trunk even though the caller only ever rang in.

Set up the trunk in Twilio Console

Configure the trunk in the Twilio Console. Unmute does not create or attach carrier resources. Twilio’s step-by-step guide shows the same Console screens.
1

Open the trunk

In Twilio Console, go to Elastic SIP Trunking, Manage, Trunks. Open the trunk used by LiveKit, or create one if none exists. Do not change a shared trunk that is serving another route.A new trunk starts with every feature off, so none of the steps below are inherited from a trunk you set up before.
2

Set termination and authentication

On Termination, choose a unique Termination SIP URI ending in pstn.twilio.com. Under Authentication, select or create a Credential List with a username and password. Save the trunk.This is the dial-out side. Its domain and credential become three of your four SIP values.
3

Point origination at LiveKit

On Origination, select Add new Origination URI. Paste your project’s SIP address and append ;transport=tcp, for example sip:abc123def.sip.livekit.cloud;transport=tcp. Keep it enabled, then add and save it.The host is your LiveKit project id without its p_ prefix. Read the id from lk project list:
A project whose id is p_abc123def has the SIP address abc123def.sip.livekit.cloud. Do not derive this value from LIVEKIT_URL or from the project’s own subdomain: the two are usually different strings, and a wrong host here gives you a number that rings and never connects.
4

Attach the phone number

First record the number’s current Voice routing so you can restore it later. On the trunk’s Numbers tab, choose Associate a Number with this Trunk, select your voice-capable number, choose this SIP trunk for Voice, and save. A number attached to a trunk ignores its normal voice webhook.You can do the same from the number’s own page: Phone Numbers, the number, Configuration details, then pick SIP Trunk as the handler and select this trunk.
5

Allow calls to transfer

In the trunk’s General settings, enable Call Transfer (SIP REFER) and tick Enable PSTN Transfer. Then set Caller ID for Transfer Target, and read which caller ID to present before you choose, because the wrong value here fails every transfer with no useful error. Save the trunk.
If your number and trunk live in a Twilio Region such as Ireland, the console and the API show you only that region. Twilio Regions says what changes.

Copy the four SIP values

The connection file names the environment variables that will hold them, never the values:
connections/twilio_sip.yaml
The names are plain SIP names rather than Twilio ones because the generated code can dial through any SIP carrier with them. Put the four values in .env, and list the four names under secrets: in agent.yaml.

Every key this connection takes

Two scalars and four names. Nothing else belongs in the file.
sip
required
The mechanism. sip is this route. The other three, connector, cloud-websocket and daily-sip, are other routes with other keys.
twilio | telnyx | plivo
required
The carrier account behind the trunk. This page is twilio, and the other two take the same four names because they are standard SIP names.
environment variable name
required
Holds the trunk’s termination domain, the one ending in pstn.twilio.com.
environment variable name
required
Holds the username from the trunk’s credential list.
environment variable name
required
Holds that credential’s password.
environment variable name
required
Holds the attached number in E.164 form. The generated setup script reads this one line to find your number.
Every value is a name, never a value, and each name is UPPER_SNAKE. An account_sid or an auth_token here is refused: those belong to the routes that call Twilio’s REST API, and this one authenticates to the trunk.

Deploy the agent first

LiveKit Cloud walks the deploy. It comes before the LiveKit records because the names the agent, and a rule that names an agent nobody has registered sends the call nowhere.

Create the LiveKit records

Setting up the trunk in Twilio gets the call as far as LiveKit. LiveKit then rejects it, because nothing in your project claims that number yet. Two records fix that, and your build generates both along with the script that creates them:
Three things about the script are worth knowing:
  • It needs lk and jq on your PATH. It checks for both and names the missing one before it creates anything.
  • It reads your number, never your secrets. It takes SIP_FROM_NUMBER from the environment, or reads that one line out of .env as text. It never sources the file.
  • Running it again is safe. It finds both records by your phone number rather than by an id, so nothing has to be copied anywhere, and whatever already exists is reused. Each line it prints ends in (created) or (reused).
Check what it made:
A number that no inbound trunk lists is a number LiveKit will not answer for.

Call the number

Call it. The agent answers and greets you. Speak, and it replies.

What a rename breaks

An ordinary redeploy leaves all of this alone: the trunk, the rule, and the number keep working. Renaming the agent does not. The dispatch rule names the worker as a string, the package’s name: joined to the target, and no deploy updates it. The number then rings and nothing answers, while the agent reports healthy: it was never dispatched. Running the script again does not repair it, because the script reuses the rule that already names your trunk. Renaming the agent breaks that rule walks the swap.

One number, one route

A number attached to a SIP trunk ignores its voice configuration, so it cannot also point at the TwiML Bin the Pipecat route uses. Take the number off the trunk to test that route, and put it back to test this one. If you want both routes live at once, give each one its own number. Two numbers cost less attention than swapping one.

If the call does not arrive

Work backwards: does lk sip inbound list show a trunk claiming your number, does lk sip dispatch list show a rule for it naming this agent, and does lk agent status show the agent up. A missing trunk or rule means the setup script has not run.
A LiveKit SIP call is answered when something in the room publishes audio, so a job that dies before the agent starts speaking leaves the caller listening to ringing until LiveKit gives up three minutes later.The usual cause is a secret that is declared but not deployed. The generated agent checks its phone environment the moment it sees a SIP caller, so a missing name fails the job there, on a real call only, and never in the browser dev loop. Compare the two lists:
Every name in the second belongs in the first. Add what is missing to .env, then lk agent update-secrets --secrets-file .env, which restarts the agent.
Check the trunk’s Origination tab. The URI must be your project’s SIP address with ;transport=tcp, and that host is your LiveKit project id without its p_ prefix, not the subdomain in LIVEKIT_URL.
The trunk is presenting a caller ID the destination’s carrier rejects. Caller ID for a transfer target has the two settings and how to tell them apart in Twilio’s call log.

The other LiveKit route: the connector

LiveKit also reaches Twilio without a trunk. On the connector transport, a generated bridge speaks Twilio Media Streams over a WebSocket and joins the call into a LiveKit room, where the same agent worker answers. The number’s voice webhook points at POST /telephony/inbound on the bridge, so the connection file names the account trio instead of the four SIP values:
connections/twilio_connector.yaml

Every key the connector connection takes

connector
required
The mechanism. It is what puts the bridge in the emitted project.
twilio
required
The only carrier this route has.
environment variable name
required
Holds the account SID, the one starting AC.
environment variable name
required
Holds the account’s auth token.
environment variable name
required
Holds your number in E.164 form, which is the caller identity a dialled person sees.
What changes:
  • You host the bridge. One container runs the agent worker and the bridge web server, reachable at the HTTPS origin you put in UNMUTE_PUBLIC_URL, and it connects out to a LiveKit Server you run. There is no SIP trunk and no Redis.
  • No transfers. A transfer needs a SIP participant and an outbound trunk, and this route has neither, so a cold or warm transfer is refused at validation. Inbound, outbound and hangup work.
  • One more call fact. The bridge supplies everything the SIP route does plus stream_id.
The emitted build/livekit/README.md for a connector target carries the endpoint, the variables, and how to scale it.

Where to go next

Transfers on this route

Cold and warm transfer over the trunk you just configured.

Pipecat over Twilio

The cloud-websocket route end to end.