When a customer texts your support number or calls your business line, you need to respond instantly—not after your polling script checks for new messages. Twilio webhooks solve this problem by sending real-time HTTP notifications to your server the moment events occur, enabling you to build responsive SMS bots, interactive voice response systems, and automated notification pipelines.
Whether you're building two-factor authentication, appointment reminders, customer support chatbots, or call routing systems, Twilio webhooks are the foundation of real-time communication. This guide covers everything you need to implement Twilio webhooks securely, from basic setup to production-ready code with signature verification.
Common Twilio webhook use cases:
- SMS auto-responders and chatbots for customer service
- Two-factor authentication (2FA) code delivery and verification
- Appointment reminders and confirmation workflows
- Voice call routing and interactive voice response (IVR) menus
- Delivery status tracking for transactional messages
- Real-time alerts and notification systems
By the end of this guide, you'll know how to set up Twilio webhooks, verify signatures using HMAC-SHA1, handle SMS and Voice events, and implement production-ready endpoints. Plus, you can test your integration with our Webhook Payload Generator without exposing your local environment.
What Are Twilio Webhooks?
Twilio webhooks are HTTP callbacks that Twilio sends to your application when communication events occur. Unlike traditional API polling where you repeatedly check for updates, webhooks push event data to your server instantly, enabling real-time responses to customer interactions.
Here's how Twilio webhooks work:
[Customer sends SMS] → [Twilio receives message] → [Twilio POSTs to your webhook] → [Your app processes and responds] → [Twilio delivers your response]
What makes Twilio webhooks different:
-
Bidirectional communication: Some webhooks (like incoming SMS or calls) expect TwiML responses with instructions, while others (like status callbacks) are fire-and-forget notifications.
-
Form-encoded data: Unlike many modern webhooks that send JSON, Twilio sends
application/x-www-form-urlencodedPOST data, which affects signature verification. -
TwiML responses: For incoming messages and calls, you respond with Twilio Markup Language (XML) to control conversation flow, not JSON.
-
HMAC-SHA1 signatures: Twilio uses HMAC-SHA1 (not SHA-256) with a unique signing process that includes the full URL and sorted parameters.
Prerequisites before you start:
- Active Twilio account with phone number(s)
- Auth Token from your Twilio Console (for signature verification)
- Publicly accessible HTTPS endpoint (or ngrok for local testing)
- Basic understanding of HTTP POST requests and XML
The main advantage of webhooks over polling is efficiency—your server only processes events when they occur, reducing API calls by 99% while enabling instant responses to customer interactions.
Setting Up Twilio Webhooks
Setting up Twilio webhooks takes just a few minutes in the Twilio Console. Here's the step-by-step process:
Step 1: Access Your Twilio Console
Log in to console.twilio.com and navigate to Phone Numbers > Manage > Active Numbers. Select the phone number you want to configure.
Step 2: Configure Messaging Webhooks
Scroll down to the Messaging Configuration section. You'll see two webhook fields:
- A Message Comes In: This webhook fires when someone sends an SMS or MMS to your Twilio number
- Primary Handler Fails (Fallback): Backup URL if your primary webhook is unreachable
Enter your webhook URL in the format:
https://yourdomain.com/webhooks/twilio/sms
Select HTTP POST as the method (this is the default and recommended option).
Step 3: Configure Voice Webhooks
In the Voice Configuration section, configure:
- A Call Comes In: Webhook for incoming voice calls
- Primary Handler Fails (Fallback): Backup URL for call handling
- Call Status Changes: Optional status callback URL for call events
Enter your voice webhook URL:
https://yourdomain.com/webhooks/twilio/voice
Step 4: Save Your Configuration
Click Save at the bottom of the page. Your webhooks are now active and will receive events immediately.
Step 5: Retrieve Your Auth Token
To verify webhook signatures, you need your Auth Token:
- Go to your Twilio Console Dashboard
- Find the Account Info section
- Click the eye icon next to Auth Token to reveal it
- Copy this token—you'll use it as your HMAC signing secret
Important: Keep your Auth Token secret! Anyone with this token can verify webhook signatures and access your Twilio account.
Pro Tips for Webhook Setup
- Use separate endpoints: Configure different URLs for SMS (
/webhooks/twilio/sms) and Voice (/webhooks/twilio/voice) for easier debugging and routing - HTTPS is required: Self-signed certificates won't work—use Let's Encrypt or a valid CA-signed certificate
- Test mode: Use ngrok during development, but remember to update URLs to production endpoints before launch
- Status callbacks: For message delivery tracking, set the
StatusCallbackparameter when sending messages via API - Rate limits: Twilio enforces rate limits on webhook deliveries. If you consistently timeout or error, Twilio may disable your webhooks
- Check the debugger: The Twilio Console includes a Debugger showing all webhook requests, responses, and errors
Testing Your Webhook Configuration
Twilio doesn't provide a built-in "test webhook" button, but you can verify your setup by:
- Sending a test SMS to your Twilio number from your phone
- Calling your Twilio number
- Checking the Twilio Debugger for webhook delivery logs
- Using our Webhook Payload Generator to send signed test payloads
Once configured, Twilio will POST to your endpoints whenever events occur. Now let's look at what data Twilio sends in these webhook requests.
Twilio Webhook Events & Payloads
Twilio sends different webhook payloads depending on the event type. Here are the most common webhook events with full payload examples:
Webhook Event Types Overview
| Event Type | Webhook Purpose | Response Expected |
|---|---|---|
| Incoming SMS | Customer sends text message | TwiML with <Message> or empty response |
| SMS Status Callback | Message delivery status update | 200 OK or empty TwiML |
| Incoming Voice Call | Customer calls your number | TwiML with <Say>, <Gather>, etc. |
| Call Status Callback | Call progress updates | 200 OK or empty TwiML |
| WhatsApp Message | Customer sends WhatsApp message | TwiML with <Message> |
Event 1: Incoming SMS Message
Description: Fires when someone sends an SMS or MMS to your Twilio number. Your application must respond with TwiML to send a reply (or empty TwiML to silently acknowledge).
Webhook Parameters (Form-Encoded POST Data):
MessageSid=SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MessagingServiceSid=MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From=+15558675309
To=+15551234567
Body=Hello, I need support with my order
NumMedia=0
NumSegments=1
FromCity=SAN FRANCISCO
FromState=CA
FromZip=94105
FromCountry=US
ToCity=NEW YORK
ToState=NY
ToZip=10001
ToCountry=US
Key Fields:
MessageSid- Unique 34-character identifier for this message (starts with SM)AccountSid- Your Twilio account identifierFrom- Sender's phone number in E.164 format (+1XXXXXXXXXX)To- Your Twilio number that received the messageBody- Message text content (up to 1,600 characters)NumMedia- Number of media attachments (0 for SMS, >0 for MMS)NumSegments- SMS segment count (messages over 160 chars split into multiple segments)
Media Attachments (if NumMedia > 0):
MediaContentType0=image/jpeg
MediaUrl0=https://api.twilio.com/2010-04-01/Accounts/AC.../Messages/SM.../Media/ME...
MediaContentType1=image/png
MediaUrl1=https://api.twilio.com/2010-04-01/Accounts/AC.../Messages/SM.../Media/ME...
Event 2: SMS Status Callback
Description: Fires when message delivery status changes (sent, delivered, failed, etc.). These are informational webhooks—respond with 200 OK.
Webhook Parameters:
MessageSid=SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From=+15551234567
To=+15558675309
MessageStatus=delivered
Message Status Values:
queued- Message accepted and queued for deliverysending- Message transmitted to carriersent- Carrier accepted the messagedelivered- Delivery confirmed by carrier (most reliable status)failed- Delivery failed (check ErrorCode)undelivered- Carrier couldn't deliver message
Error Fields (when status is failed/undelivered):
ErrorCode=30003
ErrorMessage=Unreachable destination handset
Event 3: Incoming Voice Call
Description: Fires when someone calls your Twilio number. Respond with TwiML containing voice commands like <Say>, <Play>, <Gather>, or <Dial>.
Webhook Parameters:
CallSid=CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From=+15558675309
To=+15551234567
CallStatus=ringing
Direction=inbound
FromCity=SAN FRANCISCO
FromState=CA
FromZip=94105
FromCountry=US
ToCity=NEW YORK
ToState=NY
ToZip=10001
ToCountry=US
CallerName=JOHN SMITH
Key Fields:
CallSid- Unique identifier for this call (starts with CA)From- Caller's phone numberTo- Your Twilio numberCallStatus- Current call state:ringing,in-progress,completed,busy,failed,no-answerDirection- Call type:inbound,outbound-api,outbound-dialCallerName- Caller ID name (if available from carrier)
Event 4: Call Status Callback
Description: Fires when call status changes during the call lifecycle. Configure this URL separately in your Twilio number settings or via API parameter.
Webhook Parameters:
CallSid=CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From=+15558675309
To=+15551234567
CallStatus=completed
CallDuration=187
Direction=inbound
Timestamp=Mon, 24 Jan 2025 14:32:18 +0000
Call Status Values:
queued- Call waiting to be initiatedringing- Phone is ringingin-progress- Call connected and activecompleted- Call ended normallybusy- Destination was busyno-answer- No one answeredfailed- Call could not be completedcanceled- Outbound call was canceled before connection
Key Fields:
CallDuration- Call length in seconds (only present whenCallStatus=completed)Timestamp- When the status change occurred (RFC 2822 format)
Event 5: WhatsApp Message Received
Description: Fires when someone sends a WhatsApp message to your Twilio WhatsApp-enabled number.
Webhook Parameters:
MessageSid=SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From=whatsapp:+15558675309
To=whatsapp:+15551234567
Body=Hello from WhatsApp!
NumMedia=0
ProfileName=John Smith
WaId=15558675309
WhatsApp-Specific Fields:
ProfileName- WhatsApp display name of the senderWaId- WhatsApp ID (phone number without + prefix)From/To- Prefixed withwhatsapp:to indicate channelForwarded- "true" if message was forwarded (Boolean string)FrequentlyForwarded- "true" if frequently forwarded (Boolean string)
Important Notes:
- All phone numbers use E.164 format with + prefix
- Geographic fields (FromCity, FromState, etc.) may be empty if Twilio can't determine location
- Media URLs expire after a few days—download and store media files if needed
- Parameters vary by channel (SMS vs WhatsApp vs RCS) and may change without notice
Now that you understand what data Twilio sends, let's cover how to verify these webhook requests are genuinely from Twilio.
Webhook Signature Verification
Verifying webhook signatures is critical for security—without verification, anyone could send fake webhook requests to your endpoints, potentially triggering unauthorized actions or exposing sensitive data.
Why Signature Verification Matters
Security threats without verification:
- Attackers could spoof webhook requests to trigger SMS replies or call routing
- Malicious actors could test your business logic without authentication
- Unauthorized users could cause your system to send messages (incurring charges)
- Replay attacks could process the same event multiple times
How Twilio's signature verification works:
Twilio uses HMAC-SHA1 (Hash-based Message Authentication Code with SHA-1) to cryptographically sign every webhook request. The signature is sent in the X-Twilio-Signature HTTP header. Only someone with your Auth Token (the secret key) can generate valid signatures.
Twilio's Signature Method
Algorithm: HMAC-SHA1 with Base64 encoding
Header Name: X-Twilio-Signature
Signing Secret: Your Twilio Auth Token
Signing String Components:
- Full webhook URL (including https://, query parameters, everything)
- All POST parameters, sorted alphabetically by name
- Parameters concatenated as
name1value1name2value2(no separators)
Step-by-Step Verification Process
- Extract the signature from the
X-Twilio-Signatureheader - Retrieve your Auth Token from environment variables (never hardcode)
- Construct the signing string:
- Start with the full webhook URL (e.g.,
https://yourdomain.com/webhooks/twilio/sms) - Sort POST parameters alphabetically by parameter name
- Append each name-value pair directly to the URL:
URLname1value1name2value2
- Start with the full webhook URL (e.g.,
- Compute HMAC-SHA1:
- Use your Auth Token as the secret key
- Hash the signing string
- Base64 encode the result
- Compare signatures using constant-time comparison (prevents timing attacks)
- Validate timestamp (optional but recommended) to prevent replay attacks
Code Examples
Node.js / Express
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use express.urlencoded to parse form data, NOT express.json
app.use(express.urlencoded({ extended: false }));
app.post('/webhooks/twilio/sms', (req, res) => {
const twilioSignature = req.headers['x-twilio-signature'];
const authToken = process.env.TWILIO_AUTH_TOKEN;
// Get full URL (protocol + host + path)
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
// Create signing string: URL + sorted params
const params = req.body;
let data = url;
// Sort parameters alphabetically and concatenate
Object.keys(params).sort().forEach(key => {
data += key + params[key];
});
// Compute HMAC-SHA1 and Base64 encode
const expectedSignature = crypto
.createHmac('sha1', authToken)
.update(Buffer.from(data, 'utf-8'))
.digest('base64');
// Use timing-safe comparison
const signatureValid = crypto.timingSafeEqual(
Buffer.from(twilioSignature),
Buffer.from(expectedSignature)
);
if (!signatureValid) {
console.error('Invalid Twilio signature');
return res.status(403).send('Forbidden');
}
// Signature is valid, process webhook
const { MessageSid, From, Body } = req.body;
console.log(`Received SMS ${MessageSid} from ${From}: ${Body}`);
// Respond with TwiML (or empty response)
res.type('text/xml');
res.send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your message!</Message>
</Response>`);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Webhook server running on port ${PORT}`));
Python / Flask
import hmac
import hashlib
import base64
from flask import Flask, request, Response
from urllib.parse import urlencode
app = Flask(__name__)
AUTH_TOKEN = 'your_twilio_auth_token' # Use environment variable in production
@app.route('/webhooks/twilio/sms', methods=['POST'])
def twilio_sms_webhook():
# Get signature from headers
twilio_signature = request.headers.get('X-Twilio-Signature', '')
# Get full URL
url = request.url
# Get POST parameters as dict
params = request.form.to_dict()
# Create signing string: URL + sorted params concatenated
data = url
for key in sorted(params.keys()):
data += key + params[key]
# Compute HMAC-SHA1 with Auth Token
expected_signature = base64.b64encode(
hmac.new(
AUTH_TOKEN.encode('utf-8'),
data.encode('utf-8'),
hashlib.sha1
).digest()
).decode('utf-8')
# Verify signature with timing-safe comparison
if not hmac.compare_digest(twilio_signature, expected_signature):
print('Invalid Twilio signature')
return Response('Forbidden', status=403)
# Signature valid, process webhook
message_sid = params.get('MessageSid')
from_number = params.get('From')
body = params.get('Body')
print(f'Received SMS {message_sid} from {from_number}: {body}')
# Respond with TwiML
twiml = '''<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your message!</Message>
</Response>'''
return Response(twiml, mimetype='text/xml')
if __name__ == '__main__':
app.run(port=3000)
PHP
<?php
// Twilio webhook handler
$authToken = getenv('TWILIO_AUTH_TOKEN');
$twilioSignature = $_SERVER['HTTP_X_TWILIO_SIGNATURE'] ?? '';
// Get full URL (protocol + host + request URI)
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$url = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
// Get POST parameters
$params = $_POST;
// Create signing string: URL + sorted params
$data = $url;
ksort($params);
foreach ($params as $key => $value) {
$data .= $key . $value;
}
// Compute HMAC-SHA1 and Base64 encode
$expectedSignature = base64_encode(
hash_hmac('sha1', $data, $authToken, true)
);
// Verify signature with timing-safe comparison
if (!hash_equals($twilioSignature, $expectedSignature)) {
error_log('Invalid Twilio signature');
http_response_code(403);
die('Forbidden');
}
// Signature valid, process webhook
$messageSid = $params['MessageSid'] ?? '';
$from = $params['From'] ?? '';
$body = $params['Body'] ?? '';
error_log("Received SMS $messageSid from $from: $body");
// Respond with TwiML
header('Content-Type: text/xml');
echo '<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your message!</Message>
</Response>';
?>
Using Twilio's Official SDK (Recommended)
Instead of implementing signature verification manually, use Twilio's SDKs which handle the complexity:
Node.js:
const twilio = require('twilio');
const isValid = twilio.validateRequest(
authToken,
twilioSignature,
url,
params
);
Python:
from twilio.request_validator import RequestValidator
validator = RequestValidator(auth_token)
is_valid = validator.validate(url, params, twilio_signature)
PHP:
use Twilio\Security\RequestValidator;
$validator = new RequestValidator($authToken);
$isValid = $validator->validate($twilioSignature, $url, $params);
Common Verification Errors
❌ Using express.json() instead of express.urlencoded(): Twilio sends form data, not JSON
✅ Use express.urlencoded({ extended: false }) middleware
❌ Wrong Auth Token: Using test mode token in production or vice versa ✅ Verify token matches your current environment
❌ Incorrect URL in signature: Missing query parameters or wrong protocol ✅ Use the full URL exactly as Twilio called it
❌ Not sorting parameters: Parameters must be alphabetically sorted
✅ Use Object.keys(params).sort() or equivalent
❌ Using string comparison instead of timing-safe comparison: Vulnerable to timing attacks
✅ Use crypto.timingSafeEqual(), hmac.compare_digest(), or hash_equals()
❌ Framework modifying body: Some frameworks trim whitespace or modify POST data ✅ Test with Twilio's actual webhook to ensure body matches
Best Practices
- Always verify signatures before processing webhook data
- Store Auth Token in environment variables, never commit to code
- Use constant-time comparison functions to prevent timing attacks
- Log signature verification failures for security monitoring
- Consider implementing timestamp validation to prevent replay attacks (check request age)
- Use Twilio's official SDKs when possible—they handle edge cases correctly
With signature verification in place, your webhook endpoint is secure. Next, let's look at how to test your implementation locally.
Testing Twilio Webhooks
Testing Twilio webhooks during development presents a challenge: Twilio's servers need to reach your webhook endpoint, but your local development server isn't publicly accessible. Here are the best solutions:
Local Development Challenge
When developing locally, your server runs on localhost:3000 or similar, which Twilio can't reach over the internet. You need a publicly accessible URL that tunnels to your local machine.
Solution 1: ngrok (Tunneling)
ngrok creates a secure tunnel from a public URL to your localhost, allowing Twilio to reach your local development server.
Installation:
# macOS
brew install ngrok
# Windows
choco install ngrok
# Linux
wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz
tar xvzf ngrok-v3-stable-linux-amd64.tgz
sudo mv ngrok /usr/local/bin
# Verify installation
ngrok --version
Setup and Usage:
- Start your local server:
node server.js
# Server running on http://localhost:3000
- Start ngrok tunnel:
ngrok http 3000
- Copy the HTTPS URL from ngrok output:
Forwarding https://abc123def456.ngrok.io -> http://localhost:3000
- Configure webhook in Twilio Console:
SMS Webhook URL: https://abc123def456.ngrok.io/webhooks/twilio/sms
Voice Webhook URL: https://abc123def456.ngrok.io/webhooks/twilio/voice
- Test by sending an SMS or calling your Twilio number
ngrok Pro Tips:
- Use
ngrok http 3000 --log=stdoutto see all HTTP traffic in terminal - ngrok Free tier URLs change on every restart—paid plans offer fixed subdomains
- ngrok Web Interface (
http://localhost:4040) shows all requests and responses - Use
ngrok http 3000 --host-header=localhost:3000if your server checks Host header
Solution 2: Webhook Payload Generator (No Tunnel Required)
For testing webhook signature verification and business logic without exposing your local environment, use our Webhook Payload Generator.
How it works:
-
Visit the generator: Webhook Payload Generator
-
Select "Twilio" as provider from the dropdown
-
Choose event type:
- Incoming SMS Message
- SMS Status Callback
- Incoming Voice Call
- Call Status Callback
- WhatsApp Message
-
Customize payload fields:
{
"MessageSid": "SM1234567890abcdef1234567890abcdef",
"From": "+15558675309",
"To": "+15551234567",
"Body": "Test message from generator"
}
-
Enter your Auth Token: The generator computes the correct
X-Twilio-Signature -
Generate signed payload: Copy the curl command or use the built-in sender
-
Send to localhost: Test your endpoint without ngrok:
curl -X POST http://localhost:3000/webhooks/twilio/sms \
-H "X-Twilio-Signature: computed_signature_here" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "MessageSid=SM1234&From=%2B15558675309&To=%2B15551234567&Body=Test"
Benefits of the generator:
- ✅ No tunneling required—test against localhost directly
- ✅ Validates your signature verification logic
- ✅ Customize all payload fields for edge case testing
- ✅ Test error handling with malformed payloads
- ✅ Faster iteration during development
- ✅ Works offline (once you have the generator page loaded)
Solution 3: Twilio Test Credentials
Twilio provides Test Credentials that use magic phone numbers to simulate webhooks without actual SMS/Voice charges:
Magic Test Numbers:
+15005550006- Valid mobile number (will trigger webhooks)+15005550001- Invalid number (triggers error)+15005550007- Number can't receive SMS
Usage:
// Use test credentials
const accountSid = 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // Test SID
const authToken = 'your_test_auth_token';
const client = require('twilio')(accountSid, authToken);
// Send test SMS (won't actually send, but triggers webhook)
client.messages.create({
from: '+15005550006', // Magic test number
to: '+15005550006', // Magic test number
body: 'Test webhook'
});
Solution 4: Twilio Debugger & Logs
Twilio's Console includes a powerful debugging tool showing all webhook requests, responses, and errors:
Access the Debugger:
- Log into Twilio Console
- Navigate to Monitor > Logs > Debugger
- Filter by "Webhooks" or specific phone numbers
What you can see:
- Full webhook URL Twilio called
- HTTP method and headers (including X-Twilio-Signature)
- Complete POST parameters sent
- Your server's response (status code, body, headers)
- Error messages if webhook failed
- Response time (must be under 15 seconds)
Use the debugger to troubleshoot:
- Signature verification failures (check Auth Token)
- Timeout errors (response took too long)
- 404 errors (wrong webhook URL)
- Invalid TwiML responses (malformed XML)
Testing Checklist
Before deploying to production, verify your webhook implementation handles:
- Signature verification passes with valid signatures
- Signature verification rejects invalid/missing signatures
- Responds within 15 seconds (Twilio timeout limit)
- Returns valid TwiML XML for SMS/Voice webhooks
- Returns 200 OK for status callbacks
- Handles duplicate events idempotently (same event may arrive multiple times)
- Processes media attachments (MMS with NumMedia > 0)
- Handles missing optional fields gracefully
- Logs all events for debugging and audit trails
- Processes asynchronously (queues long-running tasks)
Testing Best Practices
Start simple: Test with basic SMS receiving before adding complex business logic
Use test numbers: Twilio provides test phone numbers in sandbox mode—use these first
Check logs: Always verify webhook delivery in Twilio Debugger, even if your code logs
Test failures: Intentionally return 500 errors to see how Twilio handles failures
Load test: Use tools like Apache Bench to ensure your endpoint handles webhook bursts
Security test: Try sending webhooks without signatures to verify your rejection logic
Now that you can test locally, let's build a complete production-ready webhook implementation.
Implementation Example
Here's a complete, production-ready Twilio webhook implementation with signature verification, async processing, idempotency, and comprehensive error handling.
Requirements
A production webhook endpoint must:
- Respond within 15 seconds (Twilio timeout)
- Return 200 OK status code
- Verify signature authenticity before processing
- Process asynchronously to avoid timeouts
- Handle duplicate events gracefully (idempotency)
- Return valid TwiML (for SMS/Voice) or empty response (for status callbacks)
- Log all events for debugging and audit
Full Node.js + Express Implementation
const express = require('express');
const crypto = require('crypto');
const { Queue } = require('bullmq');
const Redis = require('ioredis');
const app = express();
const redis = new Redis(process.env.REDIS_URL);
// Create queue for async webhook processing
const twilioQueue = new Queue('twilio-webhooks', { connection: redis });
// Environment configuration
const TWILIO_AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
// CRITICAL: Use urlencoded middleware for form data, NOT json
app.use(express.urlencoded({ extended: false }));
// Helper: Verify Twilio signature
function verifyTwilioSignature(req) {
const twilioSignature = req.headers['x-twilio-signature'];
if (!twilioSignature) return false;
// Construct full URL
const protocol = req.protocol;
const host = req.get('host');
const path = req.originalUrl;
const url = `${protocol}://${host}${path}`;
// Build signing string: URL + sorted params
let data = url;
const params = req.body;
Object.keys(params).sort().forEach(key => {
data += key + params[key];
});
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha1', TWILIO_AUTH_TOKEN)
.update(Buffer.from(data, 'utf-8'))
.digest('base64');
// Timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(twilioSignature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false; // Length mismatch
}
}
// Helper: Check if event already processed (idempotency)
async function isEventProcessed(eventId) {
const exists = await redis.exists(`twilio:event:${eventId}`);
return exists === 1;
}
// Helper: Mark event as processed
async function markEventProcessed(eventId) {
// Store for 24 hours to handle delayed retries
await redis.setex(`twilio:event:${eventId}`, 86400, Date.now());
}
// SMS Webhook Handler
app.post('/webhooks/twilio/sms', async (req, res) => {
try {
// 1. Verify signature FIRST
if (!verifyTwilioSignature(req)) {
console.error('Invalid Twilio signature', {
headers: req.headers,
body: req.body
});
return res.status(403).send('Forbidden');
}
// 2. Extract webhook data
const {
MessageSid,
AccountSid,
From,
To,
Body,
NumMedia,
MessageStatus
} = req.body;
// 3. Check for duplicate (idempotency)
const isDuplicate = await isEventProcessed(MessageSid);
if (isDuplicate) {
console.log(`Duplicate SMS webhook: ${MessageSid}`);
// Still return 200 to acknowledge receipt
return res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response></Response>`);
}
// 4. Mark as processing
await markEventProcessed(MessageSid);
// 5. Queue for async processing (respond fast!)
await twilioQueue.add('sms-received', {
messageSid: MessageSid,
accountSid: AccountSid,
from: From,
to: To,
body: Body,
numMedia: parseInt(NumMedia || '0'),
messageStatus: MessageStatus,
receivedAt: new Date().toISOString()
});
// 6. Return TwiML response immediately (under 15 seconds!)
console.log(`Queued SMS ${MessageSid} from ${From}`);
res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for contacting us! We'll respond shortly.</Message>
</Response>`);
} catch (error) {
console.error('SMS webhook error:', error);
// Still return 200 to prevent Twilio retries for our internal errors
res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response></Response>`);
}
});
// Voice Webhook Handler
app.post('/webhooks/twilio/voice', async (req, res) => {
try {
// 1. Verify signature
if (!verifyTwilioSignature(req)) {
console.error('Invalid Twilio signature for voice webhook');
return res.status(403).send('Forbidden');
}
// 2. Extract call data
const {
CallSid,
AccountSid,
From,
To,
CallStatus,
Direction
} = req.body;
// 3. Check for duplicate
const isDuplicate = await isEventProcessed(CallSid);
if (isDuplicate) {
console.log(`Duplicate voice webhook: ${CallSid}`);
return res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response></Response>`);
}
// 4. Mark as processing
await markEventProcessed(CallSid);
// 5. Queue for async processing
await twilioQueue.add('call-received', {
callSid: CallSid,
accountSid: AccountSid,
from: From,
to: To,
callStatus: CallStatus,
direction: Direction,
receivedAt: new Date().toISOString()
});
// 6. Return TwiML with call handling instructions
console.log(`Queued call ${CallSid} from ${From}`);
res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="alice">Thank you for calling. Your call is important to us.</Say>
<Gather action="/webhooks/twilio/voice/gather" numDigits="1" timeout="10">
<Say voice="alice">Press 1 for sales, press 2 for support, or press 3 for billing.</Say>
</Gather>
<Say voice="alice">We didn't receive a selection. Goodbye.</Say>
<Hangup/>
</Response>`);
} catch (error) {
console.error('Voice webhook error:', error);
res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="alice">We're experiencing technical difficulties. Please try again later.</Say>
<Hangup/>
</Response>`);
}
});
// Status Callback Handler (for delivery confirmations)
app.post('/webhooks/twilio/status', async (req, res) => {
try {
// Verify signature
if (!verifyTwilioSignature(req)) {
console.error('Invalid signature for status callback');
return res.status(403).send('Forbidden');
}
const {
MessageSid,
MessageStatus,
ErrorCode,
ErrorMessage
} = req.body;
// Queue status update for processing
await twilioQueue.add('status-update', {
messageSid: MessageSid,
status: MessageStatus,
errorCode: ErrorCode,
errorMessage: ErrorMessage,
timestamp: new Date().toISOString()
});
console.log(`Status update: ${MessageSid} -> ${MessageStatus}`);
// Return 200 OK (no TwiML needed for status callbacks)
res.status(200).send('OK');
} catch (error) {
console.error('Status callback error:', error);
res.status(200).send('OK'); // Still return 200
}
});
// Process queued webhooks
const { Worker } = require('bullmq');
const worker = new Worker('twilio-webhooks', async (job) => {
const { name, data } = job;
try {
switch (name) {
case 'sms-received':
await handleSMSReceived(data);
break;
case 'call-received':
await handleCallReceived(data);
break;
case 'status-update':
await handleStatusUpdate(data);
break;
default:
console.warn(`Unknown job type: ${name}`);
}
} catch (error) {
console.error(`Failed to process ${name}:`, error);
throw error; // Will trigger retry
}
}, { connection: redis });
// Business logic handlers
async function handleSMSReceived(data) {
const { messageSid, from, to, body, numMedia } = data;
console.log(`Processing SMS ${messageSid}`);
// Example: Save to database
// await db.messages.create({ messageSid, from, to, body, numMedia });
// Example: Send to CRM
// await crm.createTicket({ phone: from, message: body });
// Example: Download media attachments if present
if (numMedia > 0) {
console.log(`SMS has ${numMedia} media attachments to download`);
// await downloadTwilioMedia(messageSid, numMedia);
}
// Example: Trigger notification
// await sendSlackNotification(`New SMS from ${from}: ${body}`);
console.log(`Processed SMS ${messageSid}`);
}
async function handleCallReceived(data) {
const { callSid, from, to, callStatus, direction } = data;
console.log(`Processing call ${callSid} (${callStatus})`);
// Example: Log to analytics
// await analytics.trackEvent('call_received', { from, to, direction });
// Example: Update database
// await db.calls.create({ callSid, from, to, callStatus, direction });
console.log(`Processed call ${callSid}`);
}
async function handleStatusUpdate(data) {
const { messageSid, status, errorCode, errorMessage } = data;
console.log(`Processing status update for ${messageSid}: ${status}`);
// Example: Update delivery status in database
// await db.messages.update({
// where: { messageSid },
// data: { status, errorCode, errorMessage }
// });
// Example: Alert on failures
if (status === 'failed' || status === 'undelivered') {
console.error(`Message ${messageSid} failed: ${errorCode} - ${errorMessage}`);
// await sendAlertEmail({ messageSid, errorCode, errorMessage });
}
console.log(`Processed status update for ${messageSid}`);
}
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Twilio webhook server running on port ${PORT}`);
console.log(`SMS webhook: http://localhost:${PORT}/webhooks/twilio/sms`);
console.log(`Voice webhook: http://localhost:${PORT}/webhooks/twilio/voice`);
console.log(`Status callback: http://localhost:${PORT}/webhooks/twilio/status`);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing server...');
await worker.close();
await redis.quit();
process.exit(0);
});
Key Implementation Details
- Signature verification first - Always validate before processing any data
- Form-encoded parsing - Use
express.urlencoded(), notexpress.json() - Idempotency - Track MessageSid/CallSid to prevent duplicate processing
- Async processing - Queue webhooks with BullMQ/Redis for background jobs
- Fast response - Return TwiML within milliseconds, process business logic async
- Error handling - Return 200 even on internal errors to prevent Twilio retries
- Timing-safe comparison - Use
crypto.timingSafeEqual()to prevent timing attacks - Logging - Comprehensive logs for debugging and audit trails
- Health checks -
/healthendpoint for monitoring - Graceful shutdown - Close workers and connections on SIGTERM
Testing Your Implementation
# Install dependencies
npm install express bullmq ioredis
# Set environment variables
export TWILIO_AUTH_TOKEN="your_auth_token_here"
export TWILIO_ACCOUNT_SID="your_account_sid_here"
export REDIS_URL="redis://localhost:6379"
export PORT=3000
# Start Redis (required for queue)
redis-server
# Start webhook server
node server.js
# Test with ngrok
ngrok http 3000
# Configure webhook URL in Twilio Console
# Then send test SMS or make test call
This implementation is production-ready and handles the most common edge cases you'll encounter with Twilio webhooks.
Best Practices
Follow these best practices to build secure, reliable, and performant Twilio webhook integrations:
Security
✅ Always verify signatures - Never trust webhook data without signature verification
✅ Use HTTPS only - Twilio supports HTTP, but HTTPS prevents man-in-the-middle attacks
✅ Store Auth Token securely - Use environment variables, never commit to version control
✅ Implement rate limiting - Protect your webhook endpoints from abuse
✅ Validate input data - Sanitize and validate all webhook parameters before use
✅ Use constant-time comparison - Prevent timing attacks with crypto.timingSafeEqual()
✅ Rotate Auth Tokens periodically - Generate new tokens every 90 days
✅ Monitor for anomalies - Alert on unusual webhook patterns or signature failures
✅ Restrict by IP (optional) - While Twilio doesn't publish static IPs, firewall rules can add defense-in-depth
Performance
✅ Respond within 15 seconds - Twilio times out after 15 seconds ✅ Return 200 immediately - Acknowledge receipt fast, process async ✅ Use queue systems - BullMQ, Redis Queue, AWS SQS for background processing ✅ Avoid blocking operations - No synchronous database writes in webhook handler ✅ Download media async - MMS/WhatsApp media downloads happen in background jobs ✅ Implement exponential backoff - For retrying failed external API calls ✅ Monitor response times - Alert if webhook processing approaches timeout ✅ Scale horizontally - Use load balancers and multiple webhook servers for high volume ✅ Cache frequently accessed data - Reduce database queries during webhook processing
Reliability
✅ Implement idempotency - Track MessageSid/CallSid to prevent duplicate processing ✅ Handle duplicate webhooks - Same event may arrive multiple times due to retries ✅ Use database transactions - Ensure atomic operations for critical updates ✅ Implement retry logic - Retry failed business logic, not the webhook acknowledgment ✅ Don't rely solely on webhooks - Use polling/reconciliation jobs as backup ✅ Log all webhook events - Store raw webhook data for debugging and replay ✅ Handle missing fields gracefully - Optional parameters may be absent ✅ Test failure scenarios - Verify behavior when databases or external APIs are down ✅ Implement circuit breakers - Prevent cascading failures to external services
Monitoring
✅ Track webhook delivery rate - Monitor successful vs failed deliveries ✅ Alert on signature failures - Potential security issue or configuration error ✅ Monitor queue depth - Alert if background jobs are backing up ✅ Log event IDs - MessageSid/CallSid for traceability and support ✅ Set up health checks - Endpoint for load balancers and monitoring tools ✅ Track response times - Ensure you stay well under 15-second timeout ✅ Monitor Twilio Debugger - Check webhook errors and failed deliveries ✅ Set up error notifications - Slack/email alerts for webhook failures ✅ Track business metrics - Messages processed, calls handled, conversion rates
Twilio-Specific Best Practices
✅ Use separate endpoints - Different URLs for SMS, Voice, and status callbacks
✅ Handle all message statuses - Expect queued, sending, sent, delivered, failed, undelivered
✅ Parse E.164 phone numbers - Twilio sends numbers in E.164 format (+1XXXXXXXXXX)
✅ Return valid TwiML - Use proper XML structure with <?xml?> declaration
✅ Handle WhatsApp differently - Check for whatsapp: prefix in From/To fields
✅ Download media before expiry - Media URLs expire after a few days
✅ Use Test Credentials - Test with magic numbers to avoid charges
✅ Configure fallback URLs - Twilio calls fallback if primary webhook fails
✅ Set status callback URLs - Track message/call lifecycle with callbacks
✅ Respect carrier guidelines - Follow SHAFT compliance (no Sex, Hate, Alcohol, Firearms, Tobacco)
✅ Implement opt-out handling - Respect STOP/UNSUBSCRIBE requests immediately
✅ Use Messaging Services - Better deliverability and compliance features
Development Workflow
✅ Start with test credentials - Use Twilio sandbox before production
✅ Use ngrok for local testing - Expose localhost for webhook development
✅ Test with Webhook Payload Generator - Validate signature logic without ngrok
✅ Check Twilio Debugger - Review all webhook requests and responses
✅ Use separate accounts - Different Twilio projects for dev/staging/production
✅ Version your webhook endpoints - Use /webhooks/v1/twilio/sms for backwards compatibility
✅ Document your integration - Explain webhook flows for team members
✅ Implement feature flags - Toggle new webhook behavior without deployments
Common Pitfalls to Avoid
❌ Not verifying signatures - Critical security vulnerability ❌ Using express.json() - Twilio sends form data, not JSON ❌ Slow response times - Causes Twilio timeouts and delivery failures ❌ Synchronous processing - Blocks webhook response, causes timeouts ❌ Not handling duplicates - Same event may be delivered multiple times ❌ Hardcoding Auth Token - Security risk if code is committed ❌ Not logging events - Makes debugging production issues impossible ❌ Assuming field presence - Optional fields may be missing ❌ Not testing failures - Production errors reveal untested code paths ❌ Ignoring Twilio Debugger - Misses critical error information
By following these best practices, your Twilio webhook integration will be secure, reliable, and maintainable as your application scales.
Common Issues & Troubleshooting
Here are the most common Twilio webhook issues and their solutions:
Issue 1: Signature Verification Failing
Symptoms:
- 403 Forbidden responses to Twilio
- "Invalid signature" errors in your application logs
- Webhooks not being processed despite successful delivery
Causes & Solutions:
❌ Using wrong Auth Token (test vs production) ✅ Solution: Verify you're using the correct token from Twilio Console
# Check which token you're using
echo $TWILIO_AUTH_TOKEN
# Compare with token in Twilio Console (Account Info section)
❌ Using express.json() instead of express.urlencoded() ✅ Solution: Twilio sends form-encoded data, not JSON
// WRONG
app.use(express.json());
// CORRECT
app.use(express.urlencoded({ extended: false }));
❌ Incorrect URL in signature computation ✅ Solution: Use the exact URL Twilio called, including protocol and query parameters
// WRONG - missing protocol or using wrong host
const url = req.path;
// CORRECT - full URL with protocol
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
❌ Not sorting parameters alphabetically ✅ Solution: Parameters must be sorted before concatenating
// WRONG
Object.keys(params).forEach(key => data += key + params[key]);
// CORRECT
Object.keys(params).sort().forEach(key => data += key + params[key]);
❌ Framework trimming whitespace from POST body ✅ Solution: Some frameworks modify POST data—use raw body or Twilio's SDK
❌ Using Base64URL instead of Base64 ✅ Solution: Twilio uses standard Base64, not URL-safe Base64
Debugging Steps:
- Log the signing string you're computing
- Log the signature you received vs computed
- Verify Auth Token matches Twilio Console
- Test with a known-good payload from Webhook Payload Generator
- Check Twilio Debugger for the exact request sent
Issue 2: Webhook Timeouts
Symptoms:
- Twilio Debugger shows timeout errors (11200)
- Webhooks marked as failed despite your endpoint working
- Inconsistent webhook delivery
Causes & Solutions:
❌ Slow database queries blocking response ✅ Solution: Move database operations to async queue
// WRONG - blocks response
app.post('/webhooks/twilio/sms', async (req, res) => {
await saveToDatabase(req.body); // Takes 5 seconds
res.send('OK');
});
// CORRECT - queue for async processing
app.post('/webhooks/twilio/sms', async (req, res) => {
await queue.add('sms', req.body); // Takes 5ms
res.send('OK'); // Respond immediately
});
❌ External API calls delaying response ✅ Solution: Call external APIs in background job
// WRONG
await crm.createTicket(req.body); // Waits for CRM response
res.send('OK');
// CORRECT
res.send('OK'); // Respond first
await queue.add('create-ticket', req.body); // Process later
❌ Complex business logic in webhook handler ✅ Solution: Acknowledge immediately, process asynchronously
// Pattern: Respond fast, process slow
app.post('/webhook', async (req, res) => {
await queue.add('process', req.body); // Quick
res.send('OK'); // Fast response
// Business logic runs in background worker
});
❌ Synchronous file operations ✅ Solution: Download MMS media asynchronously
// WRONG
for (let i = 0; i < numMedia; i++) {
await downloadMedia(req.body[`MediaUrl${i}`]);
}
res.send('OK'); // Too late!
// CORRECT
res.send('OK'); // Respond first
queue.add('download-media', { numMedia, body: req.body });
Performance targets:
- Webhook handler: < 100ms
- Queue job addition: < 50ms
- Total response time: < 500ms (well under 15s timeout)
Issue 3: Duplicate Events
Symptoms:
- Same MessageSid/CallSid processed multiple times
- Duplicate SMS replies sent to customers
- Data inconsistencies in your database
Causes & Solutions:
❌ No idempotency check ✅ Solution: Track processed event IDs
// Store processed event IDs in Redis
async function isProcessed(messageSid) {
return await redis.exists(`processed:${messageSid}`);
}
async function markProcessed(messageSid) {
await redis.setex(`processed:${messageSid}`, 86400, '1');
}
// Check before processing
if (await isProcessed(messageSid)) {
return res.send('OK'); // Already handled
}
await markProcessed(messageSid);
// ... process webhook
❌ Network retries causing duplicates ✅ Solution: Twilio doesn't automatically retry, but network issues can cause perceived duplicates—always implement idempotency
❌ Multiple webhook URLs configured ✅ Solution: Check your Twilio number configuration—only one primary webhook should be set
Idempotency patterns:
- Store event IDs in Redis with 24-hour TTL
- Use database unique constraints on MessageSid/CallSid
- Use transaction locks for critical operations
- Return success for duplicate events (already processed)
Issue 4: Missing Webhooks
Symptoms:
- Expected webhooks never arrive
- Twilio Debugger shows delivery but endpoint not hit
- Intermittent webhook delivery
Causes & Solutions:
❌ Firewall blocking Twilio ✅ Solution: Ensure port 443 (HTTPS) or 80 (HTTP) is open to internet traffic
❌ Wrong webhook URL configured ✅ Solution: Verify URL in Twilio Console matches your endpoint
# Test endpoint manually
curl -X POST https://yourdomain.com/webhooks/twilio/sms \
-d "From=%2B15551234567&Body=test"
❌ SSL certificate invalid or expired ✅ Solution: Verify certificate with SSL checker
# Check certificate
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
# Twilio requires valid CA-signed certificates (not self-signed)
❌ DNS issues preventing webhook delivery ✅ Solution: Verify domain resolves correctly
# Test DNS resolution
nslookup yourdomain.com
dig yourdomain.com
# Ensure it resolves to your server's public IP
❌ Load balancer not routing to webhook endpoint ✅ Solution: Check load balancer and reverse proxy configuration
❌ Application crashed or offline ✅ Solution: Implement health checks and monitoring
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
Issue 5: Invalid TwiML Responses
Symptoms:
- Twilio Debugger shows "Invalid TwiML" errors (12100)
- Customers receive error messages
- Calls disconnect unexpectedly
Causes & Solutions:
❌ Missing XML declaration ✅ Solution: Always include <?xml?> declaration
// WRONG
res.send('<Response><Message>Hi</Message></Response>');
// CORRECT
res.send('<?xml version="1.0" encoding="UTF-8"?>\n<Response><Message>Hi</Message></Response>');
❌ Malformed XML structure ✅ Solution: Validate XML before sending
<!-- WRONG - unclosed tag -->
<Response>
<Message>Hello
</Response>
<!-- CORRECT -->
<Response>
<Message>Hello</Message>
</Response>
❌ Invalid TwiML verbs
✅ Solution: Only use valid TwiML verbs (<Say>, <Play>, <Gather>, <Dial>, etc.)
❌ Wrong Content-Type header
✅ Solution: Set Content-Type: text/xml
res.type('text/xml').send(twiml);
Issue 6: Media Attachments Not Processing
Symptoms:
- NumMedia > 0 but media URLs return errors
- Media downloads fail or timeout
- Images corrupted or missing
Causes & Solutions:
❌ Media URLs expired ✅ Solution: Download media immediately (URLs expire after a few days)
if (parseInt(req.body.NumMedia) > 0) {
// Queue media download job immediately
await queue.add('download-media', {
messageSid: req.body.MessageSid,
mediaUrls: getMediaUrls(req.body)
});
}
❌ Not including Auth credentials for media download ✅ Solution: Media URLs require basic auth with AccountSid:AuthToken
const response = await fetch(mediaUrl, {
headers: {
'Authorization': 'Basic ' + Buffer.from(
`${accountSid}:${authToken}`
).toString('base64')
}
});
❌ Incorrect media URL parsing ✅ Solution: Media parameters are numbered 0-9 (MediaUrl0, MediaUrl1, etc.)
function getMediaUrls(body) {
const urls = [];
const numMedia = parseInt(body.NumMedia || '0');
for (let i = 0; i < numMedia; i++) {
urls.push({
url: body[`MediaUrl${i}`],
contentType: body[`MediaContentType${i}`]
});
}
return urls;
}
Debugging Checklist
When troubleshooting Twilio webhook issues:
- Check Twilio Debugger for errors
- Verify webhook URL in Twilio number configuration
- Test signature verification with Webhook Payload Generator
- Check application logs for error messages
- Verify SSL certificate is valid (not self-signed)
- Test endpoint with curl to verify it's accessible
- Check response time stays under 15 seconds
- Verify Content-Type header is
text/xmlfor TwiML responses - Confirm Auth Token matches between code and Twilio Console
- Check firewall and security group settings
Most webhook issues stem from signature verification errors, timeout problems, or invalid TwiML. Use the Twilio Debugger as your first troubleshooting step—it shows exactly what Twilio sent and what your endpoint returned.
Frequently Asked Questions
Q: How often does Twilio send webhooks? A: Twilio sends webhooks immediately when events occur—typically within milliseconds for incoming SMS or calls. Status callbacks are sent when message/call states change. Twilio expects a response within 15 seconds and does NOT automatically retry failed webhook deliveries, so your endpoint must be reliable.
Q: Can I receive webhooks for past events? A: No, Twilio only sends webhooks for events as they occur in real-time. For historical data, use the Messages API or Calls API to fetch past messages and calls. Webhooks are for real-time event processing only.
Q: What happens if my endpoint is down? A: Twilio will attempt to deliver the webhook but does NOT automatically retry if it fails. Failed deliveries appear in the Twilio Debugger with error details. Configure a fallback URL in your Twilio number settings as a backup. For critical events, implement polling or reconciliation jobs to catch missed webhooks.
Q: Do I need different endpoints for test and production? A: Yes, it's strongly recommended to use separate webhook URLs for development, staging, and production environments. Each should use its corresponding Twilio project's Auth Token for signature verification. This prevents test webhooks from affecting production data and allows safe testing without customer impact.
Q: How do I handle webhook ordering?
A: Twilio does NOT guarantee webhook delivery order. Status callbacks and event webhooks may arrive out of sequence, especially under high load or network issues. Always use timestamp fields (created_at, event-specific timestamps) and design your webhook handlers to be idempotent and order-independent.
Q: Can I filter which events I receive?
A: Webhook filtering depends on the event type. For incoming SMS/calls, configure which phone numbers have webhooks. For status callbacks, specify the StatusCallback URL when sending messages or making calls. You cannot selectively filter SMS content or call attributes—all events for configured numbers/services trigger webhooks.
Q: Why do I need to return TwiML for some webhooks but not others?
A: Incoming SMS and Voice webhooks expect TwiML responses with instructions (like <Message> or <Say>) because Twilio needs to know how to handle the interaction. Status callbacks are informational only—they just report what happened, so respond with 200 OK or empty <Response/>. Think of it as: interactive events need instructions, status updates need acknowledgment.
Q: Can I use webhooks for transactional messages like 2FA codes?
A: Yes, webhooks are perfect for 2FA. When your app sends a verification code via Twilio API, set a StatusCallback URL to track delivery. You'll receive webhooks when the message is queued, sent, delivered, or failed, allowing you to retry or alert users if delivery fails. Store MessageSid to correlate status updates with specific verification attempts.
Q: How do I debug "Invalid signature" errors?
A: First, verify your Auth Token matches the one in Twilio Console. Second, ensure you're using express.urlencoded() not express.json(). Third, check your URL construction includes protocol, host, and path exactly as Twilio called it. Test with our Webhook Payload Generator using a known-good signature to isolate whether the issue is your verification logic or configuration.
Q: What's the maximum webhook response time? A: Twilio enforces a 15-second timeout. If your endpoint doesn't respond within 15 seconds, Twilio marks the delivery as failed. Best practice: respond within 100-500ms by acknowledging receipt immediately, then processing business logic asynchronously using queues. This ensures reliable webhook delivery even with complex processing requirements.
Next Steps & Resources
You now have everything you need to implement production-ready Twilio webhooks. Here's how to continue:
Try It Yourself
- Set up a Twilio account - Sign up at twilio.com (free trial available)
- Get a phone number - Purchase a phone number with SMS and Voice capabilities
- Test with our generator - Use Webhook Payload Generator to validate your signature verification
- Deploy your endpoint - Start with ngrok for development, then deploy to production with HTTPS
- Configure webhooks - Point your Twilio number to your endpoint and test with real SMS/calls
Additional Resources
Official Twilio Documentation:
- Twilio Webhooks Overview
- Webhook Security & Signature Verification
- TwiML for Programmable Messaging
- TwiML for Programmable Voice
- Twilio Debugger Console
Twilio APIs:
Developer Tools:
- Twilio CLI - Command-line tool for managing Twilio resources
- Twilio SDKs - Official libraries for Node.js, Python, PHP, Ruby, Java, C#
- Twilio Test Credentials - Magic phone numbers for testing
Related InventiveHQ Guides
- Webhooks Explained: Complete Guide - Understanding webhooks fundamentals
- Webhook Signature Verification Guide - Deep dive into signature algorithms
- Test Webhooks Locally with ngrok - Local development best practices
Tools & Testing
- Webhook Payload Generator - Create test payloads with valid Twilio signatures
- ngrok - Expose localhost to internet for webhook testing
- Postman - API testing and webhook simulation
- RequestBin - Inspect webhook payloads
Need Help?
- Twilio Support - support.twilio.com
- Twilio Community - community.twilio.com
- Stack Overflow - Tag questions with
[twilio] - Twilio Status - status.twilio.com for service health
- InventiveHQ Contact - Get free consultation for webhook integration help
Keep Learning
Consider building these projects to master Twilio webhooks:
- SMS Auto-responder - Reply to keywords with automated responses
- Two-factor authentication - Implement SMS verification codes
- IVR phone menu - Build interactive voice response system
- Appointment reminders - Send SMS reminders with delivery tracking
- Customer support chatbot - AI-powered SMS conversations
- Call recording service - Record and store business calls
- SMS notification system - Real-time alerts for app events
Start with a simple SMS echo bot (receive message, respond with same message) to validate your setup, then gradually add complexity.
Conclusion
Twilio webhooks enable real-time, bidirectional communication between your application and customers via SMS, voice calls, WhatsApp, and other channels. By following this guide, you now know how to:
- ✅ Set up Twilio webhooks in your account dashboard
- ✅ Verify webhook signatures using HMAC-SHA1 securely
- ✅ Implement production-ready webhook endpoints with async processing
- ✅ Handle SMS, Voice, WhatsApp events with proper TwiML responses
- ✅ Test webhooks locally with ngrok and our payload generator
- ✅ Troubleshoot common issues like signature failures and timeouts
Remember the key principles:
- Always verify signatures using HMAC-SHA1 with your Auth Token
- Respond within 15 seconds by acknowledging immediately and processing async
- Process idempotently to handle duplicate webhook deliveries gracefully
- Use form-encoded parsing (
express.urlencoded) not JSON parsing - Return valid TwiML for SMS/Voice events, 200 OK for status callbacks
Next steps:
- Start with a simple SMS echo bot to validate your setup
- Use our Webhook Payload Generator to test signature verification
- Gradually add business logic with queue-based async processing
- Monitor webhook delivery in Twilio Debugger
- Implement comprehensive logging for production troubleshooting
Twilio webhooks power millions of messages and calls daily for businesses worldwide. With proper signature verification, fast responses, and robust error handling, your integration will be secure and reliable at any scale.
Ready to build? Test your first webhook integration with our Webhook Payload Generator or contact us for expert guidance on implementing Twilio webhooks in your application.
Have questions or run into issues? Check the Twilio Community, review the official documentation, or reach out to Twilio Support for assistance.