Source: https://docs.cumuluslabs.io/phone
Content version: a452333b

# Phone

Phone agents call out over your own carrier: connect a SIP trunk, import the numbers you own on it, and place calls from them. Talos does not sell numbers or bill for carrier minutes.

## Bring your own carrier {#byoc}

### In Twilio {#twilio}

1.  Create an **Elastic SIP trunk** and turn on **Secure Trunking**: Talos dials over TLS (port 5061) with SRTP media.
2.  Under **Termination**, set a Termination SIP URI (`<name>.pstn.twilio.com`) and attach a **Credential List** (a username and password). IP access control lists on their own are not supported: Talos authenticates with those digest credentials.
3.  Under **Numbers**, add every number you will call from.

### In Talos {#connect}

The simplest path is Talos: **Phone numbers → Connect trunk**, then **Import number** for each caller ID. It stores the password as a secret for you, so the password never passes through a script or an assistant. The API steps are below.

Setting up telephony takes an **admin** key. A trunk is your carrier's SIP termination (`twilio_elastic_sip` or `generic_sip`) with digest credentials: the username, and the password stored as a secret of kind `sip_auth`. Its host must resolve to public addresses.

**Connect a trunk and import a number**

```
# 1. Store the trunk's SIP digest password as a secret (admin key).
PUT /api/v1/secrets/twilio-sip          { "kind": "sip_auth", "value": "…" }
#    → { "id": "twilio-sip", "current_version": "…", … }

# 2. Connect the trunk. Talos dials out through it with these credentials.
POST /api/v1/telephony/trunks
{ "trunk_id": "twilio-main", "name": "Twilio main", "provider": "twilio_elastic_sip",
  "termination_uri": "acme.pstn.twilio.com", "auth_username": "talos",
  "auth_secret": { "id": "twilio-sip", "version": "<current_version>" } }

# 3. Import a number you own on that trunk, and assign it to an agent.
POST /api/v1/telephony/numbers
{ "e164": "+14155550100", "trunk_id": "twilio-main", "nickname": "Collections line",
  "outbound_agent_id": "<phone agent id>" }
#    → { "number_id": "…", "state": "pending" | "ready", … }
```

*   A number is `pending` until it is provisioned, then `ready`. Calls from a pending number are refused with `503 provider_unavailable`; retry once it is ready.
*   `outbound_agent_id` records which published agent calls from the number; an agent with a number assigned cannot be deleted.
*   Inbound calls to your numbers are not available yet: `inbound_agent_id` and inbound trunks are refused.
*   Trunks and numbers belong to one environment: connect them in your sandbox to test calls there, and again in production.

> **What Cumulus sets up first**
> 
> Cumulus staff set each environment's concurrent call cap and open it for runs. Before that, connecting a trunk or importing a number is refused with `tenant_not_configured`, and calls with `tenant_not_configured` or `tenant_disabled` (`403 forbidden`, in `details.reason`). `GET /api/v1/runs/capacity` shows the limit and whether admission is open. Ask your Cumulus contact to set voice capacity and open run admission for the environment (its slug and `tenant_id` are in `GET /api/v1/environments`). Your calls always go out on your own trunk; you never need a Cumulus trunk.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`GET /api/v1/telephony/trunks`](https://docs.cumuluslabs.io/reference/trunks.list) | `trunks_list` | viewer | List BYOC SIP trunks |
| [`POST /api/v1/telephony/trunks`](https://docs.cumuluslabs.io/reference/trunks.create) | `trunks_create` | admin | Connect a BYOC SIP trunk |
| [`PATCH /api/v1/telephony/trunks/{trunk_id}`](https://docs.cumuluslabs.io/reference/trunks.update) | `trunks_update` | admin | Update a BYOC SIP trunk |
| [`DELETE /api/v1/telephony/trunks/{trunk_id}`](https://docs.cumuluslabs.io/reference/trunks.delete) | `trunks_delete` | admin | Disconnect a BYOC SIP trunk |
| [`GET /api/v1/telephony/numbers`](https://docs.cumuluslabs.io/reference/numbers.list) | `numbers_list` | viewer | List BYOC phone numbers |
| [`POST /api/v1/telephony/numbers`](https://docs.cumuluslabs.io/reference/numbers.import) | `numbers_import` | admin | Import a phone number from a connected trunk |
| [`PATCH /api/v1/telephony/numbers/{number_id}`](https://docs.cumuluslabs.io/reference/numbers.update) | `numbers_update` | admin | Assign agents or rename a phone number |
| [`DELETE /api/v1/telephony/numbers/{number_id}`](https://docs.cumuluslabs.io/reference/numbers.release) | `numbers_release` | admin | Release a phone number |

## Caller ID: from {#caller-id}

A phone run picks its caller ID and trunk like this:

1.  The run's `from`, or else the `from` the agent was published with (`agent.telephony`). With neither, the run is refused: `400 invalid_request`.
2.  If that number is one you imported, the call goes out on the number's trunk.
3.  Otherwise it goes out on the trunk the agent was published with; an agent with no trunk is refused with `409 capability_unavailable`.
4.  The trunk must be yours, enabled, and allowed to present that number; if not, `403 forbidden` with `details.reason: "trunk_not_authorized"`.

> **Note**
> 
> The simplest setup: import each number you call from, and pass it as `from`. See [Place a phone call](/docs/api#phone-call).

Set an agent's trunk and default caller ID with the `agent.telephony` edit. `POST …/draft/validate` warns before any call would be refused: `telephony_trunk_unknown` (no such trunk in this environment), `telephony_from_not_imported` (the caller ID is not an imported number) and `telephony_number_pending` (the number is still being set up). Warnings do not block publishing, so you can test in the browser or with [simulated calls](/docs/agents#testing) before your trunk is ready.

> **Every phone run is a real call**
> 
> `test: true` keeps a run out of billing, webhooks and triggers, but it still dials. Test with simulated calls or a browser test session first, and call numbers you own.

## During and after a call {#calls}

*   `phone_state` follows the call: `admitted`, `preparing`, `awaiting_sip`, `dialing`, `ringing`, `active`, `terminating`, `ended`, `rejected`, `expired`, `setup_failed`.
*   Events such as `phone.amd_verdict`, `phone.voicemail` and `phone.transfer` report answering-machine detection, voicemail and transfers.
*   On an outbound call where the agent speaks first, it greets once the person has picked up: after their first words ("Hello?") and a pause, or after 1.5 s of quiet on the line, and never later than 5 s after the call connects. Those first words stay in the transcript. `beginMessageDelayMs` adds to the wait. Set `openerWaitsForCallee: false` (Talos: "Wait for the person to answer") to greet as soon as the call connects. Inbound calls and browser tests don't wait.
*   With voicemail or IVR detection set to hang up, the agent greets at answer, before detection decides, so a voicemail box can record that first line. It must be fixed text that names no one and no debt, amount or collector (for example "Hi, this is Alex on a recorded line."); ask for the person in the next node. Publishing refuses any other opener (`opener_not_neutral` on the start node), and nothing else is said until detection finds a person. The finding carries a fix in `fixes` that puts such a greeting in front of your opener: send its `edits` with `drafts.edit` (Talos's Problems list does this with one click). A leave-message policy waits for detection before its first word, so its opener is not limited.
*   Phone agents record both channels by default. `GET …/recording` returns a link that expires after five minutes; `GET …/transcript` the turns with their timing.
*   A recording never ends or shortens a call. If recording cannot start by the time the call is answered, or stops partway, the call continues unrecorded, the run's `recording.updated` event reports `failed` with a reason, and `GET …/recording` reads `failed`.
*   Concurrent calls are capped per organization (`GET /api/v1/runs/capacity`, channel `phone`).

## Voices {#voices}

`GET /api/v1/voices` lists the speech-to-text and text-to-speech models and the voices an agent can use, optionally for one language (`en` or `es`). Set them on a draft with the `agent.provider` edit.

| Operation | MCP tool | Key role | Purpose |
| --- | --- | --- | --- |
| [`GET /api/v1/voices`](https://docs.cumuluslabs.io/reference/voices.list) | `voices_list` | viewer | List the speech models and platform TTS voices an agent can use |
