← Twilio Developer KitCONTENT HISTORY

Update to Twilio Developer Kit

Snapshot Sep 30, 2026 · 22:50 UTC · version 0.2.2

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full 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