← Twilio Developer KitCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Twilio Developer Kit
Snapshot Sep 30, 2026 · 22:50 UTC · version 0.2.2
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "twilio-voice-twiml",
"description": "Build voice call logic using TwiML (Twilio Markup Language). Covers the core verbs (Say, Play, Gather, Dial, Record, Conference), generating TwiML with Python and Node.js SDKs, and a complete inbound call IVR example. Use this skill to define call behavior for inbound or outbound calls.",
"included_files": [
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 217
}
],
"skill_md_contents": "---\nname: twilio-voice-twiml\ndescription: >\n Build voice call logic using TwiML (Twilio Markup Language). Covers the\n core verbs (Say, Play, Gather, Dial, Record, Conference), generating TwiML\n with Python and Node.js SDKs, and a complete inbound call IVR example. Use\n this skill to define call behavior for inbound or outbound calls.\n---\n\n## Overview\n\nTwiML is XML that Twilio executes during a call. Your server returns a TwiML document in response to a Twilio webhook POST, and Twilio executes it.\n\n```\nCaller → Twilio → POST to your webhook → Your server returns TwiML → Twilio executes it\n```\n\n---\n\n## Prerequisites\n\n- Twilio account with a voice-capable phone number\n — New to Twilio? See `twilio-account-setup`\n- Webhook endpoint returning TwiML with `Content-Type: text/xml`\n- SDK (for programmatic generation): `pip install twilio` / `npm install twilio`\n\n---\n\n## Quickstart\n\nA minimal inbound call handler that greets the caller and presents a menu:\n\n**Python (Flask)**\n```python\nfrom flask import Flask, request\nfrom twilio.twiml.voice_response import VoiceResponse\n\napp = Flask(__name__)\n\n@app.route(\"/voice\", methods=[\"POST\"])\ndef handle_call():\n response = VoiceResponse()\n gather = response.gather(num_digits=1, action=\"/menu-choice\")\n gather.say(\"Welcome to Acme. Press 1 for sales, 2 for support.\")\n response.redirect(\"/voice\") # Loop if no input\n return str(response)\n\n@app.route(\"/menu-choice\", methods=[\"POST\"])\ndef menu_choice():\n digit = request.form.get(\"Digits\")\n response = VoiceResponse()\n if digit == \"1\":\n response.dial(\"+15551234567\")\n elif digit == \"2\":\n response.say(\"Connecting to support.\")\n response.dial(\"+15559876543\")\n else:\n response.say(\"Invalid option.\")\n response.redirect(\"/voice\")\n return str(response)\n```\n\n**Node.js (Express)**\n```node\nconst { VoiceResponse } = require(\"twilio\").twiml;\n\napp.post(\"/voice\", (req, res) => {\n const response = new VoiceResponse();\n const gather = response.gather({ numDigits: 1, action: \"/menu-choice\" });\n gather.say(\"Welcome. Press 1 for sales, 2 for support.\");\n response.redirect(\"/voice\");\n res.type(\"text/xml\").send(response.toString());\n});\n\napp.post(\"/menu-choice\", (req, res) => {\n const digit = req.body.Digits;\n const response = new VoiceResponse();\n if (digit === \"1\") response.dial(\"+15551234567\");\n else response.say(\"Invalid option.\").redirect(\"/voice\");\n res.type(\"text/xml\").send(response.toString());\n});\n```\n\n---\n\n## Core Verbs\n\n### Say — Text-to-speech\n\n**Python**\n```python\nfrom twilio.twiml.voice_response import VoiceResponse\n\nresponse = VoiceResponse()\nresponse.say(\"Your appointment is confirmed.\", voice=\"alice\", language=\"en-US\")\n```\n\n**Node.js**\n```node\nconst { VoiceResponse } = require(\"twilio\").twiml;\nconst response = new VoiceResponse();\nresponse.say({ voice: \"alice\", language: \"en-US\" }, \"Your appointment is confirmed.\");\n```\n\nVoices: `alice` (default), `man`, `woman`, or Polly/Google TTS (e.g. `Polly.Joanna`).\n\n### Gather — Collect keypad input or speech\n\n**Python**\n```python\nresponse = VoiceResponse()\ngather = response.gather(num_digits=1, action=\"/handle-input\", method=\"POST\")\ngather.say(\"Press 1 for sales, press 2 for support.\")\nresponse.say(\"We did not receive your input.\") # Fallback if no input\n```\n\n**Node.js**\n```node\nconst gather = response.gather({ numDigits: 1, action: \"/handle-input\", method: \"POST\" });\ngather.say(\"Press 1 for sales, press 2 for support.\");\nresponse.say(\"We did not receive your input.\");\n```\n\nTwilio POSTs collected digits to `action` as `Digits` parameter.\n\n### Play — Play an audio file\n\n**Python**\n```python\nresponse = VoiceResponse()\nresponse.play(\"https://example.com/audio/greeting.mp3\")\n```\n\n**Node.js**\n```node\nconst response = new VoiceResponse();\nresponse.play(\"https://example.com/audio/greeting.mp3\");\n```\n\nSupported formats: MP3, WAV. URL must be publicly accessible.\n\n### Dial — Connect to another number\n\n**Python**\n```python\nfrom twilio.twiml.voice_response import Dial\n\nresponse = VoiceResponse()\ndial = Dial(action=\"/dial-complete\")\ndial.number(\"+15558675310\")\nresponse.append(dial)\n```\n\n**Node.js**\n```node\nconst dial = response.dial({ action: \"/dial-complete\" });\ndial.number(\"+15558675310\");\n```\n\n### Record — Capture caller audio\n\n**Python**\n```python\nresponse = VoiceResponse()\nresponse.say(\"Leave a message after the beep.\")\nresponse.record(\n action=\"/recording-complete\",\n max_length=60,\n transcribe=True,\n transcribe_callback=\"/transcription-ready\"\n)\n```\n\n**Node.js**\n```node\nconst response = new VoiceResponse();\nresponse.say(\"Leave a message after the beep.\");\nresponse.record({\n action: \"/recording-complete\",\n maxLength: 60,\n transcribe: true,\n transcribeCallback: \"/transcription-ready\",\n});\n```\n\n### Voicemail — Record a message when no one answers\n\nUse `<Dial>` with `action` URL + `<Record>` in the action handler. When the dial times out or the callee is busy, the action URL serves TwiML with `<Record>`.\n\n**Python**\n```python\n# Primary TwiML — try to connect the call\nresponse = VoiceResponse()\ndial = Dial(action=\"/voicemail\", timeout=20) # 20 seconds before voicemail\ndial.number(\"+15558675310\")\nresponse.append(dial)\n\n# /voicemail handler — plays if no answer\ndef voicemail_handler(request):\n response = VoiceResponse()\n response.say(\"We missed your call. Please leave a message after the beep.\")\n response.record(\n action=\"/recording-complete\",\n max_length=120,\n transcribe=True,\n transcribe_callback=\"/transcription-ready\",\n play_beep=True\n )\n response.say(\"We didn't receive a recording. Goodbye.\")\n return str(response)\n```\n\n**Node.js**\n```node\n// Primary TwiML — try to connect the call\nconst response = new VoiceResponse();\nconst dial = response.dial({ action: \"/voicemail\", timeout: 20 });\ndial.number(\"+15558675310\");\n\n// /voicemail handler — plays if no answer\napp.post(\"/voicemail\", (req, res) => {\n const response = new VoiceResponse();\n response.say(\"We missed your call. Please leave a message after the beep.\");\n response.record({\n action: \"/recording-complete\",\n maxLength: 120,\n transcribe: true,\n transcribeCallback: \"/transcription-ready\",\n playBeep: true,\n });\n response.say(\"We didn't receive a recording. Goodbye.\");\n res.type(\"text/xml\").send(response.toString());\n});\n```\n\n**Important:** `<Record>` captures the caller only (voicemail-style). It is NOT for recording two-party calls — see `twilio-call-recordings` for that.\n\n### Conference — Multi-party calls\n\n**Python**\n```python\nresponse = VoiceResponse()\ndial = response.dial()\ndial.conference(\n \"Daily Standup\",\n start_conference_on_enter=True,\n end_conference_on_exit=True\n)\n```\n\n**Node.js**\n```node\nconst response = new VoiceResponse();\nconst dial = response.dial();\ndial.conference(\"Daily Standup\", {\n startConferenceOnEnter: true,\n endConferenceOnExit: true,\n});\n```\n\n### Pay — PCI-compliant payment collection\n\n> **Critical warnings:**\n> - Pay Connectors are **Console-only** — there is no REST API to create or manage connectors. Set up in Console > Voice > Pay Connectors before coding.\n> - **PCI Mode is IRREVERSIBLE** once enabled on an account. Use a dedicated sub-account for payment calls.\n\n**Python**\n```python\nresponse = VoiceResponse()\nresponse.say(\"We'll now collect your payment.\")\npay = Pay(\n payment_connector=\"stripe_connector\", # Name from Console setup\n charge_amount=\"49.99\",\n currency=\"usd\",\n action=\"/payment-complete\",\n status_callback=\"/payment-status\"\n)\nresponse.append(pay)\n```\n\n**Node.js**\n```node\nconst response = new VoiceResponse();\nresponse.say(\"We'll now collect your payment.\");\nresponse.pay({\n paymentConnector: \"stripe_connector\",\n chargeAmount: \"49.99\",\n currency: \"usd\",\n action: \"/payment-complete\",\n statusCallback: \"/payment-status\",\n});\n```\n\nSupported processors: Stripe, Braintree, CardConnect. Card data routes directly to the processor — never touches your server.\n\n---\n\n## Production Deployment\n\n### Webhook Hosting\n\nFor production, do NOT use ngrok. Deploy your TwiML server with HTTPS:\n\n- **Requirement**: Public HTTPS URL, responds within 15 seconds, returns `Content-Type: text/xml`\n- **Options**: Cloud Run, AWS Lambda + API Gateway, Railway, Render — any service with TLS and auto-scaling\n- **Fallback URL**: Configure in Console (Phone Numbers > Active Numbers > select number) for when your primary server is unreachable\n\n### State Between TwiML Requests\n\nEach webhook request is stateless. To maintain conversation state across interactions:\n\n- **URL query params**: Pass state in `action` URLs — `/next-step?language=es&dept=sales`\n- **Session store**: Use Redis or a database keyed by `CallSid`\n- **Do NOT use in-memory state** — your server may scale to multiple instances\n\n### Monitoring\n\n- **Status callbacks**: Track call lifecycle events (`statusCallback` on the call or number config)\n- **Voice Insights**: Automatic quality metrics per call (Console > Monitor > Insights)\n- **Debugger**: Console > Monitor > Errors for TwiML parsing failures and webhook timeouts\n- **Fallback URLs**: Always configure a fallback TwiML URL — serves a graceful message if your primary endpoint fails\n\n---\n\n## Webhook Request Parameters\n\n| Parameter | Description |\n|-----------|-------------|\n| `CallSid` | Unique call identifier |\n| `From` | Caller's number |\n| `To` | Called number |\n| `CallStatus` | Current status |\n| `Direction` | `inbound` or `outbound-api` |\n\n---\n\n## CANNOT\n\n- **Cannot return TwiML without correct content type** — Must use `Content-Type: text/xml`\n- **Cannot exceed 15-second webhook response time** — Twilio times out and falls back\n- **Cannot exceed 4,096 characters in `<Say>` verb** — Split longer text across multiple `<Say>` elements\n- **Cannot create Pay Connectors via API** — Pay Connectors are Console-only (Console > Voice > Pay Connectors). No REST API exists for connector management.\n- **Cannot reverse PCI Mode** — Once enabled on an account, PCI Mode is permanent and account-wide. Use a dedicated sub-account for payment calls.\n- **Cannot use `<Record>` for two-party call recording** — `<Record>` captures the caller only (voicemail-style). For dual-channel recording of both parties, use `record=True` on `calls.create()` or the Recordings API.\n\n---\n\n## Next Steps\n\n- **Place outbound calls (AMD, conferencing):** `twilio-voice-outbound-calls`\n- **AI voice agents with real-time speech/LLM:** `twilio-voice-conversation-relay`\n"
}SHA-256: 65529aa2f1a77f1f75b6801bd8da1bc210eac01ffbef85ac4bed3dbfbd8e76ee