# Webhooks
Twilio sends webhook requests to your application when events happen — calls
connect, messages are received, status changes occur, etc. This guide covers
verifying and handling those requests.
## Signature Verification
Every webhook request includes an `X-Twilio-Signature` header. Always verify
it before trusting the payload to prevent spoofed requests.
### Form-Encoded Webhooks
Most Twilio webhooks send form-encoded POST bodies:
```elixir
url = "https://myapp.com/twilio/voice"
params = conn.params # %{"CallSid" => "CA123", "From" => "+14158675310", ...}
signature = get_req_header(conn, "x-twilio-signature") |> List.first()
auth_token = Application.fetch_env!(:twilio_elixir, :auth_token)
if Twilio.Webhook.valid?(url, params, signature, auth_token) do
handle_call(params)
else
send_resp(conn, 403, "Invalid signature")
end
```
### JSON Body Webhooks
Some newer Twilio webhooks send JSON bodies. These use a different
verification method — a SHA256 hash of the body is appended to the URL:
```elixir
url = "https://myapp.com/twilio/status"
body = conn.assigns.raw_body # the raw request body string
signature = get_req_header(conn, "x-twilio-signature") |> List.first()
auth_token = Application.fetch_env!(:twilio_elixir, :auth_token)
if Twilio.Webhook.valid_body?(url, body, signature, auth_token) do
event = JSON.decode!(body)
handle_event(event)
else
send_resp(conn, 403, "Invalid signature")
end
```
## How Verification Works
Twilio's signature algorithm:
1. Take the full webhook URL (including scheme, host, port, and path)
2. For form-encoded bodies: sort the POST parameters alphabetically by key,
then append each key-value pair to the URL
3. For JSON bodies: compute the SHA256 hash of the raw body, append it to the
URL as `?bodySHA256=<hash>`
4. HMAC-SHA1 the resulting string using your Auth Token as the key
5. Base64-encode the result
The `X-Twilio-Signature` header contains this Base64-encoded HMAC. The SDK
uses constant-time comparison to prevent timing attacks.
## Phoenix Integration
### Basic Controller
```elixir
defmodule MyAppWeb.TwilioController do
use MyAppWeb, :controller
@auth_token Application.compile_env!(:twilio_elixir, :auth_token)
def voice(conn, params) do
url = current_url(conn)
signature = get_req_header(conn, "x-twilio-signature") |> List.first()
if Twilio.Webhook.valid?(url, params, signature, @auth_token) do
xml = Twilio.TwiML.VoiceResponse.new()
|> Twilio.TwiML.VoiceResponse.say("Hello! Thanks for calling.")
|> Twilio.TwiML.VoiceResponse.to_xml()
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
else
send_resp(conn, 403, "Forbidden")
end
end
def message(conn, params) do
url = current_url(conn)
signature = get_req_header(conn, "x-twilio-signature") |> List.first()
if Twilio.Webhook.valid?(url, params, signature, @auth_token) do
from = params["From"]
body = params["Body"]
Logger.info("SMS from #{from}: #{body}")
xml = Twilio.TwiML.MessagingResponse.new()
|> Twilio.TwiML.MessagingResponse.message("Thanks for your message!")
|> Twilio.TwiML.MessagingResponse.to_xml()
conn
|> put_resp_content_type("text/xml")
|> send_resp(200, xml)
else
send_resp(conn, 403, "Forbidden")
end
end
defp current_url(conn) do
MyAppWeb.Endpoint.url() <> conn.request_path
end
end
```
### Router
```elixir
# lib/my_app_web/router.ex
scope "/twilio" do
post "/voice", MyAppWeb.TwilioController, :voice
post "/message", MyAppWeb.TwilioController, :message
end
```
## URL Considerations
The URL used for verification **must exactly match** the URL Twilio sends
the request to, including:
- **Scheme** (`https://` not `http://`)
- **Host** (the public-facing hostname, not `localhost`)
- **Port** (include non-standard ports like `:8443`)
- **Path** (exact match, including trailing slashes)
If you're behind a reverse proxy or load balancer, make sure you reconstruct
the URL from the original request, not the proxied one. Using your
endpoint's configured URL (as shown above) is usually the safest approach.
## Status Callbacks
When you create a call or message, you can specify a `StatusCallback` URL.
Twilio will send webhooks as the resource's status changes:
```elixir
{:ok, message} = Twilio.Api.V2010.MessageService.create(client, %{
"To" => "+15551234567",
"From" => "+15559876543",
"Body" => "Hello!",
"StatusCallback" => "https://myapp.com/twilio/status"
})
```
Status callback webhooks are verified the same way as other webhooks — check
the `X-Twilio-Signature` header.
## Common Webhook Parameters
### Voice Webhooks
| Parameter | Description |
|-----------|-------------|
| `CallSid` | Unique identifier for the call |
| `From` | Caller's phone number |
| `To` | Called phone number |
| `CallStatus` | `ringing`, `in-progress`, `completed`, `busy`, `no-answer`, `failed` |
| `Direction` | `inbound` or `outbound-api` |
### Messaging Webhooks
| Parameter | Description |
|-----------|-------------|
| `MessageSid` | Unique identifier for the message |
| `From` | Sender's phone number |
| `To` | Recipient's phone number |
| `Body` | Message text |
| `NumMedia` | Number of media attachments |
| `MediaUrl0` | URL of the first media attachment |
## Tips
- **Always verify signatures.** Never trust webhook data without checking
`X-Twilio-Signature`.
- **Respond quickly.** Twilio expects a response within 15 seconds for voice
webhooks or the call will fail. Process events asynchronously if needed.
- **Return TwiML.** Voice and messaging webhooks expect an XML response.
See the [TwiML guide](twiml.md) for building responses.
- **Handle duplicates.** Network retries can cause the same webhook to arrive
more than once. Use `CallSid` or `MessageSid` as an idempotency key.
- **Use HTTPS.** Twilio will only send webhooks to HTTPS URLs in production.
For local development, use [ngrok](https://ngrok.com) or similar.