This section as Markdown: View Download

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

ModeStatusNotes
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-channel enabled) — Agent Overview.
  • A Twilio account with at least one purchased phone number.

Setup

Step 1: Get your Twilio Live Credentials

  1. Twilio Console → Account → Keys & Credentials → API keys & tokens.
  2. 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.

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 whose VoiceUrl was successfully written.
  • twilioWebhookSkipped — a single reason string, returned instead of twilioWebhooksUpdated when the VoiceUrl step 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

FieldTypeRequiredDescription
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
  }'
ParameterTypeRequiredDescription
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
    }
  ]
}
FieldTypeDescription
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:

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 reason no matching IncomingPhoneNumber on Twilio account confirms 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/Logs for errors.
  • Caller hears your fallback message but no agent: maxcallseconds was reached before the agent connected, or the agent crashed mid-call. Check POST /UserAgent/Logs.
  • Whitelisted caller still gets busy signal: Ensure E.164 format. Twilio normalizes inbound numbers — if your test phone presents itself as 4155551234 instead 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.