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.
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.- Two things are called a trunk - which trunk does what, on which side
- Set up the trunk in Twilio Console - the five console steps, and every key the connection takes
- Deploy the agent first - why the agent comes before the records
- Create the LiveKit records - the generated script, and what it makes
- Call the number - the real call
- What a rename breaks - the one change that silently unwires the route
- One number, one route - swapping a number, or buying a second
- If the call does not arrive - four failures and how to tell them apart
- The other LiveKit route: the connector - Twilio without a trunk
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 A project whose id is
;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: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.
Copy the four SIP values
The connection file names the environment variables that will hold them, never
the values:
connections/twilio_sip.yaml
.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.
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
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
lkandjqon 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_NUMBERfrom the environment, or reads that one line out of.envas 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).
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’sname: 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
the call rings and drops with no agent
the call rings and drops with no agent
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.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 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.the number never reaches LiveKit at all
the number never reaches LiveKit at all
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.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 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 theconnector 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.
- 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.
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.