Overview
TwiML 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.
Prerequisites
- Twilio account with a voice-capable phone number
— New to Twilio? See
twilio-account-setup - Webhook endpoint returning TwiML with
Content-Type: text/xml - SDK (for programmatic generation):
pip install twilio/npm install twilio
Quickstart
A minimal inbound call handler that greets the caller and presents a menu:
Python (Flask)
Node.js (Express)
Core Verbs
Say — Text-to-speech
Python
Node.js
Voices: alice (default), man, woman, or Polly/Google TTS (e.g. Polly.Joanna).
Gather — Collect keypad input or speech
Python
Node.js
Twilio POSTs collected digits to action as Digits parameter.
Play — Play an audio file
Python
Node.js
Supported formats: MP3, WAV. URL must be publicly accessible.
Dial — Connect to another number
Python
Node.js
Record — Capture caller audio
Python
Node.js
Voicemail — Record a message when no one answers
Use <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>.
Python
Node.js
Important: <Record> captures the caller only (voicemail-style). It is NOT for recording two-party calls — see twilio-call-recordings for that.
Conference — Multi-party calls
Python
Node.js
Pay — PCI-compliant payment collection
Critical warnings:
- Pay Connectors are Console-only — there is no REST API to create or manage connectors. Set up in Console > Voice > Pay Connectors before coding.
- PCI Mode is IRREVERSIBLE once enabled on an account. Use a dedicated sub-account for payment calls.
Python
Node.js
Supported processors: Stripe, Braintree, CardConnect. Card data routes directly to the processor — never touches your server.
Production Deployment
Webhook Hosting
For production, do NOT use ngrok. Deploy your TwiML server with HTTPS:
- Requirement: Public HTTPS URL, responds within 15 seconds, returns
Content-Type: text/xml - Options: Cloud Run, AWS Lambda + API Gateway, Railway, Render — any service with TLS and auto-scaling
- Fallback URL: Configure in Console (Phone Numbers > Active Numbers > select number) for when your primary server is unreachable
State Between TwiML Requests
Each webhook request is stateless. To maintain conversation state across interactions:
- URL query params: Pass state in
actionURLs —/next-step?language=es&dept=sales - Session store: Use Redis or a database keyed by
CallSid - Do NOT use in-memory state — your server may scale to multiple instances
Monitoring
- Status callbacks: Track call lifecycle events (
statusCallbackon the call or number config) - Voice Insights: Automatic quality metrics per call (Console > Monitor > Insights)
- Debugger: Console > Monitor > Errors for TwiML parsing failures and webhook timeouts
- Fallback URLs: Always configure a fallback TwiML URL — serves a graceful message if your primary endpoint fails
Webhook Request Parameters
CANNOT
- Cannot return TwiML without correct content type — Must use
Content-Type: text/xml - Cannot exceed 15-second webhook response time — Twilio times out and falls back
- Cannot exceed 4,096 characters in
<Say>verb — Split longer text across multiple<Say>elements - Cannot create Pay Connectors via API — Pay Connectors are Console-only (Console > Voice > Pay Connectors). No REST API exists for connector management.
- Cannot reverse PCI Mode — Once enabled on an account, PCI Mode is permanent and account-wide. Use a dedicated sub-account for payment calls.
- Cannot use
<Record>for two-party call recording —<Record>captures the caller only (voicemail-style). For dual-channel recording of both parties, userecord=Trueoncalls.create()or the Recordings API.
Next Steps
- Place outbound calls (AMD, conferencing):
twilio-voice-outbound-calls - AI voice agents with real-time speech/LLM:
twilio-voice-conversation-relay
