sip transport with the twilio carrier. A call on it takes four
hops:
On this page:
- Before you start - the tools, and the values you look up once
- Two things are called a trunk - which trunk does what
- 1. Write the route into the package - the channel, the connection, and every key it takes
- 2. Deploy the agent - why the agent comes before the records
- 3. Set up the trunk in Twilio - the five console steps, including the number
- 4. Create the two LiveKit records - two commands, from the generated JSON
- Call the number - the real call, and the only proof there is
- Take it down - stop the calls, or remove the route
- What to redo, and when - which steps a change costs you
- If the call does not arrive - the failures, and what each one really is
- The other LiveKit route: the connector - Twilio without a trunk
- Where to go next
Your own
build/livekit/README.md is the authority for your build. It names
your agent, your number’s variable, and the same commands with your values
filled in. This page is the same route with the reasons attached.Before you start
You need three things on your machine: the LiveKit CLI (lk), the Twilio CLI
(twilio), and jq. Sign in to both once, with lk cloud auth and
twilio login.
Then look up three values. Every command below uses one of them:
sip-dispatch-rule.json
carries it.
Two things are called a trunk
This route needs one trunk in Twilio and one in LiveKit. They do different jobs, and having one is not having the other:
LiveKit refuses a call whose number no inbound trunk claims, however right the
Twilio side is. So finishing the Twilio side is only half the job.
The Twilio trunk itself has two sides, on two tabs that sound alike and do
opposite things:
An inbound-only agent still fills all four SIP values, because the route wants
them whichever direction it uses. A cold transfer needs the trunk too, even
though the caller only ever rang in.
1. Write the route into the package
Three files. The channel says the agent answers the phone, the connection says how the call arrives, and the target picks that connection.agent.yaml
capacity.peak_starts_per_second required, so add it
in the same edit. Phone calls
has the rest of the capacity: keys.
connections/twilio_sip.yaml
targets.yaml
sip-inbound-trunk.json and sip-dispatch-rule.json into
build/livekit/. Those two files are what step 4 sends to LiveKit, and they are
the only thing under build/ you fill in by hand. For everything else, change
the package and compile again.
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. It has to be the number you attach to
the trunk in step 3.
account_sid or an auth_token is refused: those belong to the routes that call
Twilio’s REST API, and this one signs in to a trunk instead.
Put the four values in .env.
Get your Twilio details
says which console field fills which name.
2. Deploy the agent
create
once, then lk --project "$LK_PROJECT" agent deploy . for every later change.
deploy updates the agent named in livekit.toml, and Version and
Deployed At in agent list both change when it worked.
The agent comes before the LiveKit records because the
names it. A rule that names an agent nobody deployed
opens a room and leaves the caller listening to ringing.
Pass --secrets-file .env whenever the list of names changes. The four SIP_*
names are new to a package that had no phone route, and the generated agent
checks its whole declared list the moment a SIP caller arrives. A missing name
fails the call, not the build.
3. Set up the trunk in Twilio
Twilio owns the number and answers the phone network. The origination URL is the forwarding address that hands the call to LiveKit. Unmute creates none of this. Twilio’s own guide shows the same console screens.1
Open or create the trunk
In the Twilio Console, go to Elastic SIP Trunking, Manage,
Trunks. Open the trunk you want to use, or create one. Do not change a
shared trunk that is already serving another route.A new trunk starts with every feature off, so nothing below is inherited
from a trunk you set up before.
2
Set termination and authentication
On the Termination tab, choose a 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 are three of your four
SIP values.3
Point origination at LiveKit
First write down your project’s SIP address. It comes from the project ID:Take the ID of the project your agent deploys to. It starts with Do not build this address from
p_. Drop
the p_, put the rest in front of .sip.livekit.cloud, and add
;transport=tcp:Then add that address to the trunk. In the console: the Origination tab,
Add new Origination URI, paste it, leave priority and weight at 1, tick
Enabled, save. From the CLI:
LIVEKIT_URL. The project URL subdomain and
the SIP subdomain are different strings, and a wrong host here gives you a
number that rings and never connects.Self-hosted LiveKit: there is no project SIP address. Point origination at
the public SIP signalling address of the LiveKit SIP service you deployed.4
Attach the number to the trunk
This is the step people miss, because you do it from inside the trunk and
not from the number.Open the trunk, go to its Numbers tab, and click Add a Number. Some
accounts label the button Add an Existing Number. Tick your number in
the list and save. The Numbers tab should then show it.Check it from the CLI:Write down the number’s old Voice configuration first. A number attached to
a trunk ignores its own Voice configuration, silently, so it cannot also
serve a webhook or a TwiML Bin.
5
Allow calls to transfer
Skip this one if your agent never hands a caller to a person.In the trunk’s General settings, turn on Call Transfer (SIP REFER)
and tick Enable PSTN Transfer. Then set Caller ID for Transfer
Target. Read which caller ID to
present before you
choose: the wrong value fails every transfer with no useful error. Save the
trunk.
4. Create the two LiveKit records
Twilio now forwards the call, and LiveKit rejects it, because nothing in your project claims that number yet. Two records fix that:
Both are created from the JSON that compiling wrote into
build/livekit/. Each
file holds one placeholder you fill in by hand.
The inbound trunk. Open sip-inbound-trunk.json and put your number in
place of ${SIP_FROM_NUMBER}, so the line reads:
ST_.
The dispatch rule. Open sip-dispatch-rule.json and put that ID in place of
${UNMUTE_SIP_TRUNK_ID}. The rest of the file, your agent’s name and the room
prefix, is already filled in. Then:
ST_ ID, fix that before running the second. A
rule with the placeholder still in it matches every trunk in the project.
Then check both:
Use the JSON, not the lk flags
Two flag sets look like they do the same job and do not.sip dispatch create --individualhas no flag for the agent, so it makes a rule with an empty Agents column. The room opens, nothing joins it, and the caller hears ringing until LiveKit gives up.sip inbound create --auth-user/--auth-passmakes a trunk that asks incoming calls for a SIP password. Twilio origination sends none, because it identifies itself by source IP, so LiveKit rejects every call. The generated JSON sets no authentication, which is what this route wants.
allowedAddresses on the trunk. Never use digest authentication here, because
Twilio will not answer it.
Both records belong to one project and one number. A new project needs its own
pair, and so does a new number.
Call the number
Call it from a phone. The agent answers and greets you. Speak, and it replies. That call is the only proof of the phone path.unmute dev is a
browser session, and it stops exactly where the phone leg starts. There is no
local SIP rehearsal. Watch the call arrive with:
Take it down
To stop calls reaching the agent and keep everything else, remove the origination URL. The number and both trunks stay as they are:phone channel from agent.yaml,
drop connection: from targets.yaml, delete connections/twilio_sip.yaml, and
compile again. Remove the four SIP_* names from secrets: too, or the agent
will demand values nothing uses.
What to redo, and when
An ordinary redeploy leaves the trunk, the rule and the number alone. Renaming
the agent does not. The dispatch rule holds the agent name as plain text, and no
deploy updates it. The number then rings while the agent reports healthy,
because nothing ever dispatched it. Renaming the agent breaks that
rule walks the swap.
If the call does not arrive
The symptom is what you see. The cause is rarely next to it.
Two more worth knowing:
.envis not in git. It follows neither a branch nor a worktree, so each checkout needs its own copy. Everything in it is uploaded by--secrets-file .env, so keep it to the names you declared.- A trunk’s name changes nothing. A trunk you inherited from an old demo works fine once it claims the right number and asks for no inbound password. Renaming it is cosmetic. The authentication setting is not.
the call rings and rings, and nobody ever answers
the call rings and rings, and nobody ever answers
A LiveKit SIP call is answered when something in the room publishes audio.
So a job that dies before the agent speaks leaves the caller listening to
ringing until LiveKit gives up.The usual cause is a secret that is declared but not deployed. The generated
agent checks its phone environment the moment a SIP caller arrives, so a
missing name fails the job there, on a real call only. Compare the two
lists:Every name in the second belongs in the first. Add what is missing to
.env, then run lk --project "$LK_PROJECT" agent update-secrets --secrets-file .env, which restarts the agent.a transfer fails at once with 486 Busy Here
a transfer fails at once with 486 Busy Here
The trunk is presenting a caller ID the other 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 theconnector transport, a
generated bridge speaks Twilio Media Streams over a WebSocket and joins the call
into a LiveKit room, where the same agent 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.
- 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. 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 validation refuses a cold or warm transfer. Inbound, outbound and hangup work.
- One more call fact. The bridge supplies everything the SIP route does,
plus
stream_id.
build/livekit/README.md for a connector target carries the
endpoint, the variables, and how to scale it.
Where to go next
Get your Twilio details
Which console value fills which name, on every Twilio route.
Transfers on this route
Cold and warm transfer over the trunk you just set up.
Go live on LiveKit Cloud
The deploy in step 2, in full.
Pipecat over Twilio
The
cloud-websocket route end to end.