# TwiML
TwiML (Twilio Markup Language) is the XML that tells Twilio how to handle
incoming calls and messages. This library provides a functional builder API
for generating TwiML without writing raw XML.
## Voice Responses
Use `Twilio.TwiML.VoiceResponse` to build voice call instructions:
```elixir
alias Twilio.TwiML.VoiceResponse
xml = VoiceResponse.new()
|> VoiceResponse.say("Welcome to our phone system.")
|> VoiceResponse.to_xml()
```
Output:
```xml
Welcome to our phone system.
```
## Voice Verbs
### Say
Text-to-speech with optional voice and language:
```elixir
VoiceResponse.new()
|> VoiceResponse.say("Hello!", voice: "alice", language: "en-US")
|> VoiceResponse.to_xml()
```
### Play
Play an audio file or DTMF tones:
```elixir
VoiceResponse.new()
|> VoiceResponse.play("https://example.com/welcome.mp3")
|> VoiceResponse.play("ww12", digits: true)
|> VoiceResponse.to_xml()
```
### Gather
Collect user input (keypress or speech):
```elixir
VoiceResponse.new()
|> VoiceResponse.gather(
num_digits: 1,
action: "/handle-key",
method: "POST",
children: [
{"Say", %{}, ["Press 1 for sales. Press 2 for support."]}
]
)
|> VoiceResponse.say("We didn't receive any input. Goodbye!")
|> VoiceResponse.to_xml()
```
The `children` option lets you nest verbs inside ``. Each child is a
tuple of `{tag_name, attributes_map, [content]}`.
### Dial
Connect the caller to another phone number, SIP endpoint, or client:
```elixir
# Dial a number
VoiceResponse.new()
|> VoiceResponse.dial("+15551234567", caller_id: "+15559876543")
|> VoiceResponse.to_xml()
# Dial with nested nouns
VoiceResponse.new()
|> VoiceResponse.dial(nil,
caller_id: "+15559876543",
children: [
{"Number", %{}, ["+15551111111"]},
{"Number", %{}, ["+15552222222"]},
{"Client", %{}, ["agent_jane"]}
]
)
|> VoiceResponse.to_xml()
```
Supported nested nouns: `Number`, `Client`, `Sip`, `Queue`.
### Record
Record the caller's voice:
```elixir
VoiceResponse.new()
|> VoiceResponse.say("Please leave a message after the beep.")
|> VoiceResponse.record(
max_length: 30,
action: "/handle-recording",
transcribe: true,
transcribe_callback: "/handle-transcription"
)
|> VoiceResponse.to_xml()
```
### Redirect
Transfer control to another TwiML URL:
```elixir
VoiceResponse.new()
|> VoiceResponse.redirect("/next-step")
|> VoiceResponse.to_xml()
```
### Control Verbs
```elixir
VoiceResponse.new()
|> VoiceResponse.pause(length: 2) # Pause for 2 seconds
|> VoiceResponse.hangup() # End the call
|> VoiceResponse.to_xml()
```
Other control verbs: `reject/1` (reject with reason), `enqueue/2` (add to
queue).
## Messaging Responses
Use `Twilio.TwiML.MessagingResponse` for incoming SMS/MMS replies:
```elixir
alias Twilio.TwiML.MessagingResponse
xml = MessagingResponse.new()
|> MessagingResponse.message("Thanks for your message!")
|> MessagingResponse.to_xml()
```
### Message with Media
Send MMS with media attachments:
```elixir
MessagingResponse.new()
|> MessagingResponse.message("Here's a photo!", media: "https://example.com/photo.jpg")
|> MessagingResponse.to_xml()
```
### Multiple Messages
```elixir
MessagingResponse.new()
|> MessagingResponse.message("Message 1")
|> MessagingResponse.message("Message 2")
|> MessagingResponse.to_xml()
```
### Redirect
Redirect to another TwiML URL for the messaging response:
```elixir
MessagingResponse.new()
|> MessagingResponse.redirect("/sms/next")
|> MessagingResponse.to_xml()
```
## Attribute Naming
TwiML attributes use camelCase in XML. The builder automatically converts
snake_case Elixir options to camelCase XML attributes:
| Elixir Option | XML Attribute |
|---------------|---------------|
| `num_digits:` | `numDigits=` |
| `caller_id:` | `callerId=` |
| `max_length:` | `maxLength=` |
| `transcribe_callback:` | `transcribeCallback=` |
| `status_callback:` | `statusCallback=` |
## XML Escaping
Text content and attribute values are automatically escaped:
```elixir
VoiceResponse.new()
|> VoiceResponse.say("Tom & Jerry say \"hello\" to ")
|> VoiceResponse.to_xml()
```
Produces: `Tom & Jerry say "hello" to <everyone>`
## Phoenix Integration
Return TwiML from a Phoenix controller:
```elixir
defmodule MyAppWeb.TwilioController do
use MyAppWeb, :controller
def voice(conn, _params) do
xml = Twilio.TwiML.VoiceResponse.new()
|> Twilio.TwiML.VoiceResponse.say("Hello! Thanks for calling.")
|> Twilio.TwiML.VoiceResponse.gather(
num_digits: 1,
action: "/twilio/handle-key",
children: [
{"Say", %{}, ["Press 1 for sales. Press 2 for support."]}
]
)
|> Twilio.TwiML.VoiceResponse.to_xml()
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
end
def message(conn, %{"Body" => body}) do
reply = "You said: #{body}"
xml = Twilio.TwiML.MessagingResponse.new()
|> Twilio.TwiML.MessagingResponse.message(reply)
|> Twilio.TwiML.MessagingResponse.to_xml()
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
end
end
```
## IVR Example
A complete interactive voice response (IVR) menu:
```elixir
defmodule MyAppWeb.IVRController do
use MyAppWeb, :controller
def welcome(conn, _params) do
xml = Twilio.TwiML.VoiceResponse.new()
|> Twilio.TwiML.VoiceResponse.gather(
num_digits: 1,
action: "/twilio/menu",
children: [
{"Say", %{voice: "alice"}, [
"Welcome to Acme Corp. " <>
"Press 1 for sales. " <>
"Press 2 for support. " <>
"Press 0 to speak with an operator."
]}
]
)
|> Twilio.TwiML.VoiceResponse.say("We didn't receive any input. Goodbye!")
|> Twilio.TwiML.VoiceResponse.to_xml()
conn |> put_resp_content_type("text/xml") |> send_resp(200, xml)
end
def menu(conn, %{"Digits" => "1"}) do
xml = Twilio.TwiML.VoiceResponse.new()
|> Twilio.TwiML.VoiceResponse.say("Connecting you to sales.")
|> Twilio.TwiML.VoiceResponse.dial("+15551234567")
|> Twilio.TwiML.VoiceResponse.to_xml()
conn |> put_resp_content_type("text/xml") |> send_resp(200, xml)
end
def menu(conn, %{"Digits" => "2"}) do
xml = Twilio.TwiML.VoiceResponse.new()
|> Twilio.TwiML.VoiceResponse.say("Connecting you to support.")
|> Twilio.TwiML.VoiceResponse.enqueue("support")
|> Twilio.TwiML.VoiceResponse.to_xml()
conn |> put_resp_content_type("text/xml") |> send_resp(200, xml)
end
def menu(conn, _params) do
xml = Twilio.TwiML.VoiceResponse.new()
|> Twilio.TwiML.VoiceResponse.say("Invalid option. Please try again.")
|> Twilio.TwiML.VoiceResponse.redirect("/twilio/welcome")
|> Twilio.TwiML.VoiceResponse.to_xml()
conn |> put_resp_content_type("text/xml") |> send_resp(200, xml)
end
end
```
## Tips
- **Return TwiML quickly.** Twilio expects a response within 15 seconds for
voice webhooks.
- **Use `children` for nesting.** `Gather`, `Dial`, and `Message` support
nested elements via the `children:` option.
- **Alias for readability.** `alias Twilio.TwiML.VoiceResponse` makes
chained calls much more readable.