Skip to main content
This is the cloud-websocket transport with the twilio carrier. Your number points at a small piece of markup in the Twilio console, and that markup streams the call straight to Pipecat Cloud, which starts your agent.
This route is not SIP. There is no trunk, no webhook of yours, no tunnel, and no public URL. It shares nothing with the LiveKit Twilio route except the account your number lives in.
Your own build/pipecat/README.md is the authority for your build, because it was generated with your region and your agent name already filled in. This page is the same route with the reasons attached. Read it before the first call rather than after it.
On this page:

Before you start

You need a Twilio account with a voice-capable number, a Pipecat Cloud account, and the pipecat CLI. The Twilio CLI is optional, and it is the quickest way to check the number’s state. Then look up one value, your organisation slug:
You want the machine slug, not your display name. It looks like three-random-words-12345: lowercase, hyphenated, ending in a number. Column headings differ between CLI versions, so go by the shape of the value. It never carries the (active) marker.
This is the one value the compiler cannot know, and getting it wrong is the most common way this route fails.

1. Write the route into the package

Two files carry this route: the connection that names it, and the target that names the connection. The connection is the short one.
connections/twilio_voice.yaml

Every key a connection takes

sip | connector | cloud-websocket | daily-sip
required
The mechanism that carries the call. cloud-websocket is this route, and it is the one where Pipecat Cloud terminates the carrier’s stream itself.
twilio
required
The carrier account behind the route. This route has no other carrier.
route key to environment variable name
Left out entirely on a package that only answers calls, which is what makes the file above two lines long. A package that places a call, or hands one to a person, needs account_sid, auth_token and from_number here, and the refusal says which behaviour asked for them.

Every key a target takes

slng | livekit | pipecat
required
pipecat on this route.
exact x.y.z
required
The framework version pinned into the emitted project, written with all three numbers. targets.yaml names the one this release supports.
connection file stem
Which connection carries this target’s calls, named without the folder and without the .yaml. Required for a phone agent.
platform region name
Where the platform deploys the agent. Pipecat takes exactly one, and it is also the region in your stream address.
integer
How many instances the platform holds ready. On this route it is what makes the call answerable: see the next step.
pins, sdk_language and per-target models: are the remaining target keys, and targets.yaml covers all three. Then compile:

2. Deploy with a warm instance

targets.yaml
On this route a warm instance is not a latency nicety. It is what makes the call answerable. A cold container can take longer to start than a session stays open, so the session expires before the container is ready and the call is never picked up at all. A knowledge base makes this more likely, because the corpus is embedded at import, before the server binds. warm_instances compiles to [scaling] min_agents in pcc-deploy.toml, so every deploy of this build keeps the pool. It bills for that instance whether or not anybody calls, which is why the compiler never adds it on its own.
pipecat cloud deploy --min-agents 1 is the same thing for one deploy only, and a [scaling] block added to pcc-deploy.toml by hand does not survive the next unmute compile. Declare it in targets.yaml and the manifest carries it.
Pipecat Cloud walks the deploy and the secret set in full.

3. Point the number at your agent

Two console objects, in this order: make the Bin, then tell the number to use it.
1

Create the TwiML Bin

In the Twilio Console, go to TwiML, TwiML Bins, then the plus button. Name it anything. Paste the markup from your own build/pipecat/README.md, where the address and the agent name are already correct:
YOUR_ORGANIZATION is the slug you looked up. It is the only value you fill in by hand. Save the Bin.The <Say> line is a cold start cushion, not a requirement. Starting this agent from cold takes a few seconds, and a caller who hears nothing hangs up. Drop the line once you keep a warm instance.
2

Point the number at the Bin

Now go to Phone Numbers, Manage, Active Numbers, and open your number. Under Voice Configuration, find “A call comes in”, choose TwiML Bin from the first dropdown, and pick the Bin you just made from the second. Save.Making the Bin does not route anything on its own. This step is what connects the two, and it is the one people skip.
3

Check the number is free

A number attached to a SIP trunk ignores its Voice configuration, silently, so the Bin is never consulted. Read the state back rather than testing by ear:
An empty trunkSid is what you want. One number serves one route at a time, so buy a second number if you want the LiveKit route live as well.

Call the number

Call it from a phone. You hear the <Say> line, then the agent 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.

The values that have to agree

Three things have to line up. When one does not, the failures look nothing like each other.

The stream address carries the region

A regional stream endpoint routes only to agents deployed in that region. Your build writes the right one, because it comes from deployment_region on the target. One declaration lands in three places, and the platform needs all three to match: An agent can only read a secret set from its own region. To move region, change that one line, compile again, and paste the new address into the Bin. Pipecat’s regions guide says what each region name covers.

The service host is your agent name and your organisation slug

_pipecatCloudServiceHost is AGENT_NAME.ORGANIZATION_NAME. The agent name is filled in for you; the slug is the value from Before you start. A service host the platform refuses is rejected before it reaches your agent, so your agent’s log stays completely empty. The caller hears the spoken line and then silence. Pipecat’s own Twilio websocket guide documents the same markup shape.

The markup carries only the service host

A <Parameter> reaches this agent only if the emitted code reads one by that name, and on an inbound call that name is _pipecatCloudServiceHost alone. Adding the caller’s number or the called number here is a common suggestion, and it does nothing: no module in this package reads it.

Three different ways nothing happens

They look identical to the caller and they are easy to tell apart in the logs. Only the first one is a wiring problem. The second is warm_instances. The third is a secret that is declared but not in the set you deployed with. If Twilio’s own call log shows nothing at all, the number is not pointed at the Bin. Go back to step 3.

Reading the agent’s log

-l DEBUG filters to only DEBUG. It is not a minimum severity, so it hides the ERROR line that says why the session died. Run with no -l at all.
The CLI wraps output at 80 columns, which corrupts JSON output. For machine reading:
Two lines are worth knowing by name:
  • parse_telephony_websocket ... Parsed - Type: twilio, Data: {...} proves the Bin delivered, and shows the service host and the parameters it carried.
  • Generating chat from context [...] dumps what the model actually saw. It is the fastest way to spot echo, fragmentation, or a polluted context.

Speakerphone makes the agent interrupt itself

A phone leg has no echo cancellation. A caller on speakerphone sends the agent’s own greeting back into the microphone, it is transcribed as caller speech, and the agent cuts itself off. The garbled turn then sits in its context for the rest of the call. A Pipecat phone route protects the greeting by default for exactly this reason. Everything after the opening line stays interruptible, so tell testers to use the handset. See interruption.protect to change what is protected.

Why this route has no token

The generated pcc-deploy.toml sets websocket_auth = "none". That is not a shortcut. A TwiML Bin is static markup in a console, so it cannot fetch a token before it opens the stream. Token authentication is structurally impossible here. What limits who can start a session is knowing the AGENT.ORGANISATION string your markup carries. Treat that string like a capability. It is not a secret in the cryptographic sense, but anyone holding it can open sessions you pay for.

Where to go next

Go live on Pipecat Cloud

The deploy in step 2, in full.

Get your Twilio details

Which console value fills which name, on every Twilio route.

Transfers on this route

Hand the caller to a person.

LiveKit over Twilio

The sip route end to end.