Voice (phone calls)¶
Callers dial a phone number and talk to an agent, which answers aloud. This page is about phone calls over Twilio (Twilio Conversation Relay); in-app voice calling from the console runs on a separate stack (LiveKit, in the browser) and needs no phone number.
How it fits together¶
Caller -> Twilio number --POST /webhook/twilio/voice--> core (checks X-Twilio-Signature, mints a ticket)
Twilio <--TwiML <ConversationRelay url="wss://<core>/webhook/twilio/voice-relay">-- core
Twilio <==WebSocket (speech to text, text to speech by Twilio)==> core -> agent
- There is no separate voice credential. Voice reuses the Twilio connection from Channels > WhatsApp > Twilio (Account SID and Auth Token), and the number you point at core.
- A reply is spoken live, so it does not go through the human approval gate (a call cannot pause for an approval). What protects a call is the pairing gate below, and the agent's own tool and autonomy guardrails still apply to what it can do mid-call. There is no outbound "send" for voice.
- Spoken text is cleaned of markdown, emoji and raw URLs before Twilio reads it out.
Set it up¶
- Connect Twilio (see WhatsApp); voice, WhatsApp and SMS share it.
- In Twilio set the number's "A call comes in" webhook to
https://<your-core-host>/webhook/twilio/voice(HTTP POST). SetOMNICHANNEL_PUBLIC_BASE_URLso core can verify Twilio's signature and build thewss://URL Twilio is told to connect to. - Choose the agent that answers voice calls (the tenant's default agent for the channel), and optionally an agent per dialled number and a keypad (DTMF) menu, in Channels > Voice.
- Call the number.
Who answers, and what the caller can do¶
- Pairing. A caller is identified by their phone number. An unknown caller hears that an admin must approve them, is given a pairing code, and the call ends; after approval the next call connects. The same number keeps one memory from call to call.
- Routing precedence. A keypad digit chosen mid-call wins over the per-number agent, which wins over the tenant's default agent. These are per-call choices and never change the number's default.
- A workflow bound to calls. If a workflow is bound to the CALL trigger, the caller is asked to speak instead of being handed to the agent, and the workflow runs when they finish.
Security¶
- The inbound-call webhook must carry a valid
X-Twilio-Signaturefor the tenant's Auth Token. - The WebSocket is permitted without a login (
/webhook/**), so it is authenticated by a single-use admission ticket minted by the signature-checked call webhook: an encrypted, tamper-evident value that binds the Twilio account, call id and caller and expires after 5 minutes. The socket takes the tenant, call and caller from the ticket, ignores what the frame claims, and a ticket that was already presented is refused. - Origins. Twilio's servers send no
Origin; a browser page does, and is refused unlessapp.voice.relay.allowed-originslists it. - Twilio's voice webhooks are deliberately not de-duplicated: a Twilio retry means the call is still ringing and must be answered.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
| The call connects and is dropped at once | The ticket expired or was already used, or OMNICHANNEL_PUBLIC_BASE_URL is not core's public HTTPS address so Twilio cannot reach the wss:// URL. |
| The caller is told to ask for approval | The number is not paired yet; approve it under Channels > Pairing. |
| Every call is rejected | The signature check fails: wrong Auth Token or a mismatched OMNICHANNEL_PUBLIC_BASE_URL. |