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:
alias Twilio.TwiML.VoiceResponse
xml = VoiceResponse.new()
|> VoiceResponse.say("Welcome to our phone system.")
|> VoiceResponse.to_xml()Output:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say>Welcome to our phone system.</Say>
</Response>Voice Verbs
Say
Text-to-speech with optional voice and language:
VoiceResponse.new()
|> VoiceResponse.say("Hello!", voice: "alice", language: "en-US")
|> VoiceResponse.to_xml()Play
Play an audio file or DTMF tones:
VoiceResponse.new()
|> VoiceResponse.play("https://example.com/welcome.mp3")
|> VoiceResponse.play("ww12", digits: true)
|> VoiceResponse.to_xml()Gather
Collect user input (keypress or speech):
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 <Gather>. Each child is a
tuple of {tag_name, attributes_map, [content]}.
Dial
Connect the caller to another phone number, SIP endpoint, or client:
# 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:
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:
VoiceResponse.new()
|> VoiceResponse.redirect("/next-step")
|> VoiceResponse.to_xml()Control Verbs
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:
alias Twilio.TwiML.MessagingResponse
xml = MessagingResponse.new()
|> MessagingResponse.message("Thanks for your message!")
|> MessagingResponse.to_xml()Message with Media
Send MMS with media attachments:
MessagingResponse.new()
|> MessagingResponse.message("Here's a photo!", media: "https://example.com/photo.jpg")
|> MessagingResponse.to_xml()Multiple Messages
MessagingResponse.new()
|> MessagingResponse.message("Message 1")
|> MessagingResponse.message("Message 2")
|> MessagingResponse.to_xml()Redirect
Redirect to another TwiML URL for the messaging response:
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:
VoiceResponse.new()
|> VoiceResponse.say("Tom & Jerry say \"hello\" to <everyone>")
|> VoiceResponse.to_xml()Produces: <Say>Tom & Jerry say "hello" to <everyone></Say>
Phoenix Integration
Return TwiML from a Phoenix controller:
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
endIVR Example
A complete interactive voice response (IVR) menu:
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
endTips
- Return TwiML quickly. Twilio expects a response within 15 seconds for voice webhooks.
- Use
childrenfor nesting.Gather,Dial, andMessagesupport nested elements via thechildren:option. - Alias for readability.
alias Twilio.TwiML.VoiceResponsemakes chained calls much more readable.