> ## Documentation Index
> Fetch the complete documentation index at: https://unmute.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# Pipecat over Twilio

> The cloud-websocket route end to end: the markup your number points at, the values that have to agree, and the three ways the call goes quiet.

This is the `cloud-websocket` transport with the `twilio` carrier. Your number
points at a static piece of markup in the Twilio console, and that markup streams
the call straight to Pipecat Cloud, which starts your agent.

```text theme={null}
caller -> your Twilio number -> TwiML Bin -> wss:// stream -> Pipecat Cloud -> your agent
```

This route is **not SIP**. It shares nothing with the LiveKit Twilio route except
the account the number lives in. There is no trunk, no webhook of yours, no
tunnel, and no public URL.

<Note>
  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, and it is the page to read before
  the first call rather than after it.
</Note>

On this page:

* [Nothing of yours is hosted](#nothing-of-yours-is-hosted-and-that-decides-the-security-model) - why this route authenticates the way it does
* [The values that have to agree](#the-values-that-have-to-agree) - region, service host, markup, and the number
* [The markup](#the-markup) - what to paste into the TwiML Bin
* [What the package declares](#what-the-package-declares-for-this-route) - the connection file and the target, key by key
* [Deploy with a warm instance](#deploy-with-a-warm-instance) - the setting that makes the call answerable
* [Three different ways nothing happens](#three-different-ways-nothing-happens) - how to tell them apart in the logs
* [Reading the agent's log](#reading-the-agents-log) - the two commands, and the flag that hides the error
* [Speakerphone makes the agent interrupt itself](#speakerphone-makes-the-agent-interrupt-itself) - and what protects the greeting

## Nothing of yours is hosted, and that decides the security model

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, which makes token authentication structurally
impossible on this route.

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.

## The values that have to agree

The route fails when any of these disagree, and the failures look nothing like
each other.

### The stream address carries the region

```text theme={null}
wss://eu-central.api.pipecat.daily.co/ws/twilio   # a region is declared
wss://api.pipecat.daily.co/ws/twilio              # no region declared
```

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:

```yaml targets.yaml theme={null}
targets:
  pipecat:
    provider: pipecat
    connection: twilio_voice
    deployment_region: eu-central
```

One declaration, three places, and the platform needs all three to match:

| What                         | Where it lands                            |
| ---------------------------- | ----------------------------------------- |
| where the agent runs         | `region` in `pcc-deploy.toml`             |
| where its secrets live       | `--region` on `pipecat cloud secrets set` |
| where the carrier streams to | the `wss://` host in your markup          |

An agent can only read a secret set from its own region. To move region, change
that one line, recompile, and paste the new address into the Bin. See Pipecat's
[regions guide](https://docs.pipecat.ai/pipecat-cloud/guides/regions) for 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 organisation is the one value the compiler cannot know:

```sh theme={null}
pipecat cloud organizations list
```

<Warning>
  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.
</Warning>

This is the most common way the route fails, and the least helpful failure you
can get. 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](https://docs.pipecat.ai/pipecat-cloud/guides/telephony/twilio-websocket)
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 or the called number here is a common suggestion, and it
does nothing: no module in this package reads it.

### The number must not be on a SIP trunk

A number attached to a trunk **ignores its voice configuration**, silently, so
the Bin is never consulted. Read the state back rather than testing by ear:

```sh theme={null}
twilio api core incoming-phone-numbers list --properties phoneNumber,trunkSid,voiceUrl
```

An empty `trunkSid` is what you want. One number serves one target at a time, so
[buy a second number](/telephony/twilio#give-each-route-its-own-number) if you
want both routes live.

## The markup

Paste this from your own `build/pipecat/README.md`, where the address and the
agent name are already correct. `YOUR_ORGANIZATION` is the only value you fill
in by hand.

```xml theme={null}
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Connecting you now.</Say>
  <Connect>
    <Stream url="wss://eu-central.api.pipecat.daily.co/ws/twilio">
      <Parameter name="_pipecatCloudServiceHost" value="AGENT.YOUR_ORGANIZATION"/>
    </Stream>
  </Connect>
</Response>
```

Console path: **TwiML**, **TwiML Bins**, then the plus button. Name it anything.
Then point the number at it: **Phone Numbers**, **Manage**, **Active Numbers**,
your number, **Voice Configuration**, "A call comes in", choose **TwiML 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.

## What the package declares for this route

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

```yaml connections/twilio_voice.yaml theme={null}
transport: cloud-websocket
carrier: twilio
```

### Every key a connection takes

<ParamField path="transport" type="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.
</ParamField>

<ParamField path="carrier" type="twilio" required>
  The carrier account behind the route. This route has no other carrier.
</ParamField>

<ParamField path="environment" type="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.
</ParamField>

### Every key a target takes

The target is the block under [Deploy with a warm
instance](#deploy-with-a-warm-instance). These are its keys.

<ParamField path="provider" type="livekit | pipecat | slng" required>
  `pipecat` on this route.
</ParamField>

<ParamField path="version" type="exact x.y.z" required>
  The framework version pinned into the emitted project, written with all three
  numbers. [`targets.yaml`](/reference/targets-yaml) names the one this release
  supports.
</ParamField>

<ParamField path="connection" type="connection file stem">
  Which connection carries this target's calls, named without the folder and
  without the `.yaml`. Required for a phone agent.
</ParamField>

<ParamField path="deployment_region" type="platform region name">
  Where the platform deploys the agent. Pipecat takes exactly one, and it is
  also [the region in your stream address](#the-stream-address-carries-the-region).
</ParamField>

<ParamField path="warm_instances" type="integer">
  How many instances the platform holds ready. On this route it is what makes
  the call answerable: see [below](#deploy-with-a-warm-instance).
</ParamField>

`pins`, `sdk_language` and per-target `models:` are the remaining target keys,
and [`targets.yaml`](/reference/targets-yaml) covers all three.

## Deploy with a warm instance

```yaml targets.yaml theme={null}
targets:
  pipecat:
    provider: pipecat
    connection: twilio_voice
    deployment_region: eu-central
    warm_instances: 1
```

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.

<Warning>
  `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.
</Warning>

## Three different ways nothing happens

They look identical to the caller and they are easy to tell apart in the logs.

| Symptom                                                               | Cause                                                                        | How to confirm                                                                                                            |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| the agent log is **completely empty**, no session at all              | a wrong organisation slug, or the Bin's region does not match the deployment | `pipecat cloud agent sessions <name>` lists nothing new                                                                   |
| a session exists, marked `Complete`, with a blank `Bot Start Seconds` | the cold start outran the session window                                     | no `pipecat.workers.runner` line for that session id, and the `Uvicorn running` timestamp is later than the session's end |
| a session exists and the log has an `ERROR` line                      | your agent raised, usually a missing environment value                       | the error names it                                                                                                        |

Only the first one is a wiring problem. The second is
[`warm_instances`](#deploy-with-a-warm-instance). The third is a secret that is
declared but not in the set you deployed with.

## Reading the agent's log

```sh theme={null}
pipecat cloud agent logs <name> -n 250
pipecat cloud agent logs <name> -s <session-id>
```

<Warning>
  `-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.
</Warning>

The CLI wraps output at 80 columns, which corrupts JSON output. For machine
reading:

```sh theme={null}
COLUMNS=100000 pipecat cloud --output json agent logs <name> -n 2000 -s <session-id>
```

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 and carries the garbled turn 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`](/reference/agent-yaml) to change what is
protected.

## Where to go next

<Columns cols={2}>
  <Card title="Human transfers" icon="phone-forwarded" href="/transfers/overview">
    Cold and warm, and what each route can do.
  </Card>

  <Card title="Transfers on this route" icon="phone-forwarded" href="/transfers/pipecat-twilio">
    Hand the caller to a person.
  </Card>
</Columns>
