Twilio Voice Channel
Connect inbound phone calls (PSTN) to a Wiro agent. Used by the Voice Receptionist agent and any custom agent that needs to answer real phone calls.
Overview
Toggle int-twilio-channel on a useragent and Wiro auto-enables the bundled voice rules (util-voice-receptionist + util-voice-call-prep) via the registry's depends_on chain — you don't need to enable them separately.
Skills that use this credential:
int-twilio-channel— Twilio Voice channel.
Agents that typically enable this integration:
- Voice Receptionist (phone-receptionist for SMBs)
- Custom agents that handle inbound calls
Twilio bills you separately on your Twilio account (per inbound minute + per number rental + per TTS character when hold uses TTS). Wiro doesn't proxy Twilio billing.
Availability
| Mode | Status | Notes |
|---|---|---|
| API Key (Account SID + Auth Token) | Available | Twilio Live Credentials. Wiro auto-configures each number's VoiceUrl on save. |
Webhook auto-configuration
You don't touch the Twilio Console's webhook fields manually — when you save this credential through POST /UserAgent/CredentialUpsert (or the panel), Wiro uses your Account SID + Auth Token to auto-configure each phone number's VoiceUrl and StatusCallback. No separate "Setup webhooks" step.
Important: the number's Configure with mode in the Twilio Console must be Webhook, TwiML Bin, Function, Studio Flow, Proxy Service (Twilio's default). Wiro does not drive TwiML App or SIP Trunk routing — flip the number back to Webhook in the Twilio Console first if it's set to one of those.
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent (Voice Receptionist preset or any custom agent with
int-twilio-channelenabled) — Agent Overview. - A Twilio account with at least one purchased phone number.
Setup
Step 1: Get your Twilio Live Credentials
- Twilio Console → Account → Keys & Credentials → API keys & tokens.
- In the Live credentials section at the top of the page, copy:
- Account SID — 34 chars, starts with
AC. - Auth Token — click Show to reveal. 32-char secret.
- Account SID — 34 chars, starts with
Don't confuse with API Keys. The lower part of the same page lists API Keys — those are a different mechanism Wiro doesn't use. Use the Live Auth Token specifically — an API Key Secret pasted here will silently fail every call.
Step 2: Buy or import phone numbers
Twilio Console → Phone Numbers → Active numbers → Buy a number. Pick a number with Voice capability (toll-free, local, or international as desired).
Numbers must be in E.164 format when you save them to Wiro:
- Starts with
+ - Country code first (no leading
0) - Digits only — no spaces, dashes, or parentheses
- Max 15 digits total
Examples: +14155551234 (US), +447911123456 (UK), +905551234567 (Turkey), +5511955256325 (Brazil).
Step 3: Save credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "twilio-voice", "fieldname": "accountsid", "fieldvalue": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" },
{ "credentialkey": "twilio-voice", "fieldname": "authtoken", "fieldvalue": "your-32-character-auth-token" },
{ "credentialkey": "twilio-voice", "parentfield": "phonenumbers", "ordinal": 0, "fieldname": "value", "fieldvalue": "+14155551234" },
{ "credentialkey": "twilio-voice", "fieldname": "maxcallseconds", "fieldvalue": "600" },
{ "credentialkey": "twilio-voice", "fieldname": "holdstrategy", "fieldvalue": "text_then_music" },
{ "credentialkey": "twilio-voice", "fieldname": "holdtext", "fieldvalue": "Please hold while we connect you to our assistant." },
{ "credentialkey": "twilio-voice", "fieldname": "holdvoice", "fieldvalue": "Polly.Joanna-Neural" }
]
}'
Or save through the panel: My Agents → open agent → Credentials → Twilio Voice. The panel form auto-validates Account SID format and runs Setup webhooks on save.
Step 4: Verify webhook configuration
After saving credentials, the response includes:
twilioWebhooksUpdated— numbers whoseVoiceUrlwas successfully written.twilioWebhookSkipped— a single reason string, returned instead oftwilioWebhooksUpdatedwhen theVoiceUrlstep was skipped (e.g. no matching IncomingPhoneNumber on Twilio account).twilioWebhooksFailed— Twilio API errors per number, if any.twilioWebhookError— present instead of the three fields above when the entire auto-webhook flow throws (e.g. Twilio API outage, invalid Account SID format, network error). Holds a single error string. Treat as "no numbers were configured this round; retry the save".
You can also confirm in the Twilio Console: open the number → Voice Configuration → A call comes in should show:
- Configure with → Webhook, TwiML Bin, Function, Studio Flow, Proxy Service
- Webhook URL → a Wiro voice handler URL
- HTTP → POST
Step 5: Start the agent and place a test call
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Once the agent is in status 4 (running), dial your Twilio number from any phone. You should hear the configured hold experience for ~5–8 seconds, then the agent's greeting.
Credential Fields
| Field | Type | Required | Description |
|---|---|---|---|
accountsid |
string | Yes | Twilio Account SID — 34 chars starting with AC. |
authtoken |
string (secret) | Yes | Twilio Live Auth Token — 32-char secret. Stored encrypted at rest. |
phonenumbers |
string array | Yes | E.164-formatted phone numbers this agent will answer. All numbers share the same agent settings (hold media, persona, skills). |
maxcallseconds |
number | No | Hard cap per call in seconds. Default 600 (10 min). When reached, the call is force-ended and the caller hears a fallback message. Twilio still bills for the full duration up to this cap. |
whitelistedcallers |
string array | No | E.164-formatted caller-ID allowlist. Non-empty → only listed numbers reach the agent (others get a busy signal). Empty → all callers accepted (production default). Use to add only your own number while testing in production. |
holdstrategy |
enum | No | What the caller hears during the ~5–8 sec window it takes the agent to connect. Options: none, text, audio, music, audio_then_music, text_then_music. "X + background music" options are the most polished. |
holdtext |
string | Conditional | Required when holdstrategy ∈ {text, text_then_music}. TTS text — keep to 1 sentence (~5–8s spoken). Hard cap 3000 chars. Supports SSML tags (<break>, <prosody>, <say-as>). |
holdvoice |
enum | Conditional | TTS voice for holdtext. Empty string ("") = use Twilio account default. 20 named voices available (AWS Polly Neural / Generative + Google Chirp3-HD), each tied to a single language: Polly.Joanna-Neural (en-US), Polly.Burcu-Neural (tr-TR), Polly.Lea-Neural (fr-FR), etc. Generative tier is most human-like but priced higher per character. |
holdaudio |
file (MP3/WAV) | Conditional | Required when holdstrategy ∈ {audio, audio_then_music}. Short greeting (5–15 s recommended), mono, max 10 MB. |
holdmusic |
file (MP3/WAV) | Conditional | Required when holdstrategy ∈ {music, audio_then_music, text_then_music}. Background music that loops while the caller waits. Mono, max 10 MB. 30–60 s sweet spot. Use royalty-free music — you are responsible for licensing. |
Twilio cannot mix speech over music simultaneously — for a voice-over-music intro, pre-mix both into a single audio file and use holdstrategy: audio (or audio_then_music if you also want the loop after).
Credentials schema (as returned by POST /UserAgent/Detail)
"twilio-voice": {
"_connected": true,
"optional": false,
"extra": false,
"accountsid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"authtoken": "***encrypted***",
"phonenumbers": ["+14155551234"],
"maxcallseconds": 600,
"whitelistedcallers": [],
"holdstrategy": "text_then_music",
"holdtext": "Please hold while we connect you to our assistant.",
"holdvoice": "Polly.Joanna-Neural",
"holdaudio": null,
"holdmusic": "https://cdn.wiro.ai/uploads/.../music.mp3"
}
Call History
Retrieve the last N realtime voice sessions for a useragent — regardless of which channel they came in on.
POST /UserAgent/TwilioCallHistory/List
Despite the Twilio-named path, this endpoint returns both Twilio and Web Channel sessions. Both are stored as agent messages (metadata.type starts with realtime_session in either case), so a single feed is enough to audit every voice exchange the agent had.
curl -X POST "https://api.wiro.ai/v1/UserAgent/TwilioCallHistory/List" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"limit": 50
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Useragent instance guid. Owner / team-member access only. |
limit |
number | No | Max sessions to return. Default 50, hard-capped at 200. Sorted newest-first by message id. |
Response:
{
"result": true,
"errors": [],
"data": [
{
"messageguid": "5c41dabf-f2be-4aa8-a5a4-8c9e3d2f3f11",
"agenttoken": "8a5b9e2f-4d3c-4a01-9c2e-1b6d4e7a9c5d",
"channel": "twilio",
"callsid": "CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"callerInfo": { "number": "+15551234567", "country": "US" },
"callerProfile": "Returning caller — last asked about pricing.",
"status": "realtime_session",
"endReason": "wiro_completed",
"durationSeconds": 137,
"modelSlug": "gpt-realtime-mini",
"startedAt": 1730473321000,
"endedAt": 1730473458000
},
{
"messageguid": "0f2a1c3d-9e8b-4c5a-bf42-3a7b6e1d0c8f",
"agenttoken": "1e7d3b9a-5c2f-4d6b-8e0a-2f4c7b5d9e1a",
"channel": "web",
"callsid": "voice-call-9d2d4b6e3f6b4c1a8a7e1f5a0b2c3d4e",
"callerInfo": { "page_url": "https://example.com/contact", "display_identifier": "+15551234567" },
"callerProfile": null,
"status": "realtime_session",
"endReason": "browser_disconnect",
"durationSeconds": 42,
"modelSlug": "gpt-realtime-mini",
"startedAt": 1730463668000,
"endedAt": 1730463710000
}
]
}
| Field | Type | Description |
|---|---|---|
messageguid |
string | The guid of the message that records this session. Use it with POST /UserAgent/Message/Detail to fetch the full transcript metadata. |
agenttoken |
string | The per-message agent token. Use it on Message/Detail / Message/Cancel like any other agent message. |
channel |
"twilio" | "web" |
Source channel. Determines the callerInfo shape and whether callsid is a Twilio Call SID or a Web session id. |
callsid |
string | null | Twilio Call SID (channel: "twilio") or internal voice-call-<id> session id (channel: "web"). |
callerInfo |
object | Channel-specific. Twilio: { number, country } (E.164 phone + ISO-2 country). Web: { page_url, display_identifier? } — display_identifier is omitted unless the embedding page validated an identifier (session_metadata.display_identifier whose whole-string match passes the allowlist). |
callerProfile |
string | null | Free-text prep summary (prepSummary) the agent wrote before the audio bridge opened — used to brief the realtime model on caller context. null until prep finishes (and on rejected rows). |
status |
string | Mirrors the message's metadata.type verbatim. Values: realtime_session_incoming (call accepted, prep running), realtime_session_active (audio bridge live), realtime_session_rejected (rejected before audio, e.g. concurrent-limit), or realtime_session (no suffix — final/completed). The bare realtime_session is the only value that means "ended cleanly" — it is not the string "completed". |
endReason |
string | null | Set on completed (realtime_session) rows only. One of: wiro_completed (graceful end), wiro_cancelled, wiro_disconnect, wiro_error, max_duration (the call reached its maximum length — maxcallseconds on Twilio), browser_disconnect (Web only), twilio_disconnect (Twilio only). Rejected rows leave endReason: null and instead carry metadata.reason (concurrent_limit, agent_prep_timeout, realtime_ws_open_failed, realtime_stream_timeout) — fetch via Message/Detail. |
durationSeconds |
number | null | Wall-clock duration. null while in-progress. |
modelSlug |
string | null | Realtime model used (e.g. gpt-realtime-mini). |
startedAt / endedAt |
number | null | Unix epoch ms. endedAt stays null while the call is in-progress or rejected. |
Live call audio is billed through the int-wiro-aimodels skill against your own Wiro AI Models balance — you bring your own Wiro API key, and the voice cost flows through your Wiro project wallet. It is not charged to the agent's platform credit pool and writes no POST /UserAgent/TransactionList row. The agent's text turns around the call — the prep turn that briefs the voice model before the voice session starts, and the post-call turn where the agent summarizes and acts on the call — are billed as normal token deducts (type: "deduct", action: "tokens") and show up in TransactionList. Audit call-audio cost from your Wiro AI Models usage (the wiro project wallet); TransactionList accounts only for those text turns.
Pairing With Other Skills
Pair this channel with skills that supply the conversation behavior:
int-google-calendar— read availability + draft appointments during the call.int-hubspot-crm— match caller phone number against CRM contacts and log call outcomes.int-brevo-email/int-sendgrid-email/int-mailchimp-email— draft post-call confirmation or follow-up emails.
The Voice Receptionist preset bundles this channel together with the voice rules and Brevo/HubSpot credentials by default — see Agent Use Cases for the full preset.
Troubleshooting
- All calls fail with signature error: You probably pasted an API Key Secret instead of the live Auth Token. Re-paste from the Live credentials section of the Twilio Console.
- Number isn't in
twilioWebhooksUpdated: The number isn't on the Twilio account whose Account SID you provided. Either buy the number on this account, or import it from the source account. The skip reasonno matching IncomingPhoneNumber on Twilio accountconfirms this. - Caller hears Twilio's default voicemail: The number's Configure with is set to TwiML App or SIP Trunk — flip it back to Webhook in the Twilio Console, then re-save credentials in Wiro to re-write
VoiceUrl. - Calls cut off at exactly N seconds: That's
maxcallseconds. Raise it for longer use cases. - Hold music plays forever, agent never speaks: The agent failed to start the realtime conversation. Check
POST /UserAgent/Logsfor errors. - Caller hears your fallback message but no agent:
maxcallsecondswas reached before the agent connected, or the agent crashed mid-call. CheckPOST /UserAgent/Logs. - Whitelisted caller still gets busy signal: Ensure E.164 format. Twilio normalizes inbound numbers — if your test phone presents itself as
4155551234instead of+14155551234, the allowlist won't match. Add both forms while testing. - Rotated the Auth Token in Twilio Console: Re-paste the new token in Wiro and save again — the credential save also re-runs the webhook setup.