Twilio uses webhook URLs to deliver incoming messages, call events, and status updates to your application. Properly configured URLs ensure reliable communication handling.
Key Takeaways
- 1Webhook URLs must be publicly accessible HTTPS endpoints
- 2Twilio sends data as application/x-www-form-urlencoded
- 3Status callbacks track message and call lifecycle
- 4TwiML responses control call and message flow
- 5Validate requests with X-Twilio-Signature header
“Webhooks are user-defined HTTP callbacks. They are triggered by an event, such as receiving an SMS message, and send data to the URL configured for the webhook.”
Webhook URL Types
Twilio communicates with your application through different webhook types, each serving a specific purpose in the messaging and calling lifecycle. Understanding when each webhook fires helps you build responsive communication flows.
| Webhook | Purpose | Configuration |
|---|---|---|
| SMS Webhook | Receive incoming SMS | Phone number settings |
| Voice Webhook | Handle incoming calls | Phone number settings |
| Status Callback | Track delivery status | Per-message/call option |
| Fallback URL | Error handling | Phone number settings |
| Studio Webhook | Custom Studio actions | Studio flow widgets |
The SMS and Voice webhooks are configured on each phone number and fire for every incoming message or call. Status callbacks are optional and configured per-request, allowing you to track delivery without modifying your main webhook handler.
When someone sends an SMS to your Twilio number, Twilio makes an HTTP POST request to your webhook URL with the message details. Your endpoint must respond with TwiML instructions telling Twilio what to do next.
SMS Webhook URL
// Your webhook receives POST data from Twilio
// https://yourapp.com/webhooks/twilio/sms
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post('/webhooks/twilio/sms', (req, res) => {
// Incoming SMS data
const {
From, // Sender phone: +15551234567
To, // Your Twilio number
Body, // Message text
MessageSid, // Unique message ID
AccountSid, // Your account SID
NumMedia, // Number of media attachments
MediaUrl0, // First media URL (if any)
MediaContentType0
} = req.body;
console.log(`SMS from ${From}: ${Body}`);
// Respond with TwiML
res.type('text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your message!</Message>
</Response>
`);
});This Express handler extracts message data from the POST body and responds with TwiML XML. The important fields include From (sender's phone number), Body (message text), and MessageSid (unique identifier for tracking). Your response tells Twilio to send a reply message back to the sender.
Voice webhooks work similarly to SMS but handle phone calls instead of text messages. You respond with TwiML that controls the call flow, from playing audio to gathering keypad input or forwarding to another number.
Voice Webhook URL
// Voice webhook for incoming calls
// https://yourapp.com/webhooks/twilio/voice
app.post('/webhooks/twilio/voice', (req, res) => {
const {
From, // Caller number
To, // Called number
CallSid, // Unique call ID
CallStatus, // ringing, in-progress, completed
Direction, // inbound or outbound
CallerCity, // Caller's city (if available)
CallerState, // Caller's state
CallerCountry // Caller's country
} = req.body;
// Respond with TwiML
res.type('text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="alice">Hello! Thanks for calling.</Say>
<Gather numDigits="1" action="/webhooks/twilio/menu">
<Say>Press 1 for sales, 2 for support.</Say>
</Gather>
</Response>
`);
});
// Handle menu selection
app.post('/webhooks/twilio/menu', (req, res) => {
const { Digits } = req.body;
res.type('text/xml');
if (Digits === '1') {
res.send('<Response><Dial>+15551234567</Dial></Response>');
} else {
res.send('<Response><Dial>+15559876543</Dial></Response>');
}
});The voice handler demonstrates a multi-step call flow with the Gather verb. The action URL in the Gather element tells Twilio where to POST the caller's keypad input. This pattern of chaining webhooks lets you build complex IVR (Interactive Voice Response) systems.
Status callbacks let you track the lifecycle of messages and calls asynchronously. Unlike the main webhooks that require immediate responses, status callbacks simply notify your server about state changes like delivery confirmations or call completions.
Status Callback URLs
// Track message delivery status
const client = require('twilio')(accountSid, authToken);
// Send SMS with status callback
await client.messages.create({
body: 'Your order has shipped!',
from: '+15551234567',
to: '+15559876543',
statusCallback: 'https://yourapp.com/webhooks/twilio/status'
});
// Status callback handler
app.post('/webhooks/twilio/status', (req, res) => {
const {
MessageSid,
MessageStatus, // queued, sent, delivered, undelivered, failed
ErrorCode, // Error code if failed
ErrorMessage // Error description
} = req.body;
console.log(`Message ${MessageSid}: ${MessageStatus}`);
if (MessageStatus === 'failed' || MessageStatus === 'undelivered') {
console.error(`Error ${ErrorCode}: ${ErrorMessage}`);
}
res.sendStatus(200);
});
// Call status callback
app.post('/webhooks/twilio/call-status', (req, res) => {
const {
CallSid,
CallStatus, // queued, ringing, in-progress, completed, busy, failed, no-answer
CallDuration, // Duration in seconds
RecordingUrl // If call was recorded
} = req.body;
console.log(`Call ${CallSid}: ${CallStatus} (${CallDuration}s)`);
res.sendStatus(200);
});The code shows how to specify a statusCallback URL when sending messages and how to handle the incoming status updates. For SMS, you will receive updates as messages move through queued, sent, delivered, or failed states. For calls, you can track ringing, in-progress, and completed states along with duration data.
Security is critical for webhooks since they often trigger actions or access sensitive data. Twilio signs every request with your auth token, allowing you to verify that requests genuinely came from Twilio and were not forged by attackers.
Webhook Validation
const twilio = require('twilio');
// Middleware to validate Twilio requests
function validateTwilioRequest(req, res, next) {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const twilioSignature = req.headers['x-twilio-signature'];
// Build the full URL (must match exactly what Twilio called)
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const isValid = twilio.validateRequest(
authToken,
twilioSignature,
url,
req.body
);
if (isValid) {
next();
} else {
res.status(403).send('Invalid Twilio signature');
}
}
// Apply to all Twilio webhooks
app.use('/webhooks/twilio/*', validateTwilioRequest);
// Alternative: Use Twilio's webhook validation middleware
const { webhook } = require('twilio');
app.post('/webhooks/twilio/sms',
webhook({ validate: true }),
(req, res) => {
// Request is validated
}
);The validation middleware checks the X-Twilio-Signature header against a computed signature using your auth token. The URL must match exactly what Twilio called, including protocol and any query parameters. The built-in webhook middleware simplifies this by handling validation automatically.
Configuring webhook URLs in the Twilio console requires attention to detail. Each phone number has separate settings for messaging and voice, and you can specify fallback URLs for error handling.
URL Configuration
# Phone Number Webhook Configuration
# SMS/MMS
A MESSAGE COMES IN: Webhook
URL: https://yourapp.com/webhooks/twilio/sms
HTTP: POST (recommended)
PRIMARY HANDLER FAILS: Fallback URL
URL: https://yourapp.com/webhooks/twilio/sms-fallback
# Voice
A CALL COMES IN: Webhook
URL: https://yourapp.com/webhooks/twilio/voice
HTTP: POST (recommended)
PRIMARY HANDLER FAILS: Fallback URL
URL: https://yourapp.com/webhooks/twilio/voice-fallback
# Status Callback
CALL STATUS CHANGES: Status Callback URL
URL: https://yourapp.com/webhooks/twilio/call-statusThis configuration template shows all the webhook URL fields available in Twilio's phone number settings. Use POST for better security since GET requests expose data in URLs and server logs. The fallback URLs are called when your primary handler fails or times out.
During development, you need a way to expose your local server to Twilio's webhooks. Tools like ngrok create a secure tunnel that gives your localhost a public HTTPS URL.
Local Development with ngrok
# Start your local server
npm run dev # Listening on port 3000
# Expose with ngrok
ngrok http 3000
# Use the HTTPS URL from ngrok
# https://abc123.ngrok.io/webhooks/twilio/sms
# Or use Twilio CLI for local testing
twilio phone-numbers:update +15551234567 \
--sms-url="https://abc123.ngrok.io/webhooks/twilio/sms"After starting ngrok, copy the generated HTTPS URL and use it as your webhook endpoint in Twilio. The Twilio CLI command updates your phone number's webhook configuration programmatically, which is faster than using the web console during active development.