Twilio Webhooks Example: Handle SMS & Voice Events
Twilio webhooks notify your application when communication events occur — incoming SMS messages, voice calls, delivery status updates, and more. Instead of polling the Twilio API, webhooks push real-time event data to your endpoint.
The problem? Setting up Twilio webhooks requires signature verification, TwiML response formatting, handling the application/x-www-form-urlencoded content type, and responding within 15 seconds.
The solution? Deploy a production-ready Twilio webhook handler with Codehooks.io in under 5 minutes.
If you're new to webhooks, check out our guide on What are webhooks? to understand the fundamentals before diving into this Twilio-specific implementation.
Prerequisites
Before you begin, sign up for a free Codehooks.io account and install the CLI:
npm install -g codehooks
You'll also need:
- A Twilio account with a phone number
- Your Twilio Account SID and Auth Token (found in the Twilio Console)
Quick Start: Incoming SMS Handler
Create a new Codehooks project:
coho create my-twilio-handler
cd my-twilio-handler
npm install twilio
Replace index.js with:
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
// Bypass JWT auth for Twilio webhook endpoints
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Handle incoming SMS messages
app.post('/twilio/sms', async (req, res) => {
// Validate the request is from Twilio
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
console.error('Invalid Twilio signature');
return res.status(403).send('Forbidden');
}
// Extract message details from the webhook
const { From, To, Body, MessageSid } = req.body;
console.log(`SMS from ${From}: ${Body}`);
// Store the message in the database
const conn = await Datastore.open();
await conn.insertOne('sms_messages', {
messageSid: MessageSid,
from: From,
to: To,
body: Body,
direction: 'inbound',
receivedAt: new Date().toISOString(),
});
// Respond with TwiML
res.set('Content-Type', 'text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your message! We received: "${Body}"</Message>
</Response>
`);
});
export default app.init();
Deploy and configure:
coho deploy
coho set-env TWILIO_AUTH_TOKEN your_auth_token_here --encrypted
Your webhook URL will be: https://my-twilio-handler-xxxx.api.codehooks.io/dev/twilio/sms
Configure Twilio Console
- Go to Twilio Console → Phone Numbers
- Click on your phone number
- Under Messaging, set:
- A MESSAGE COMES IN: Webhook
- URL:
https://your-app.api.codehooks.io/dev/twilio/sms - HTTP Method: POST
- Click Save
Common Twilio Webhook Events
Incoming SMS Parameters
When Twilio receives an SMS to your number, it sends these parameters:
| Parameter | Description | Example |
|---|---|---|
MessageSid | Unique message identifier | SM1234567890abcdef |
From | Sender's phone number | +15551234567 |
To | Your Twilio number | +15559876543 |
Body | Message text content | Hello world |
NumMedia | Number of attached media files | 0 |
FromCity | Sender's city (if available) | San Francisco |
FromState | Sender's state | CA |
FromCountry | Sender's country | US |
SMS Status Callback Parameters
When you send an SMS via Twilio API with a statusCallback URL, you receive updates:
| Parameter | Description |
|---|---|
MessageSid | Unique message identifier |
MessageStatus | Current status: queued, sent, delivered, failed, undelivered |
ErrorCode | Error code if failed (e.g., 30003 for unreachable) |
ErrorMessage | Human-readable error description |
Voice Call Parameters
When someone calls your Twilio number:
| Parameter | Description |
|---|---|
CallSid | Unique call identifier |
From | Caller's phone number |
To | Your Twilio number |
CallStatus | Status: ringing, in-progress, completed, failed |
Direction | inbound or outbound-api |
SMS Status Callback Handler
Track delivery status of outbound messages:
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Handle SMS delivery status updates
app.post('/twilio/status', async (req, res) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
return res.status(403).send('Forbidden');
}
const { MessageSid, MessageStatus, ErrorCode, ErrorMessage } = req.body;
console.log(`Message ${MessageSid} status: ${MessageStatus}`);
// Update message status in database
const conn = await Datastore.open();
await conn.updateOne('sms_messages', { messageSid: MessageSid }, {
$set: {
status: MessageStatus,
errorCode: ErrorCode || null,
errorMessage: ErrorMessage || null,
updatedAt: new Date().toISOString(),
}
});
// Handle specific statuses
if (MessageStatus === 'failed' || MessageStatus === 'undelivered') {
console.error(`Message ${MessageSid} failed: ${ErrorCode} - ${ErrorMessage}`);
// Trigger retry logic or alert
}
res.status(200).send('OK');
});
export default app.init();
Voice Call Handler with TwiML
Handle incoming voice calls:
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Handle incoming voice calls
app.post('/twilio/voice', async (req, res) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
return res.status(403).send('Forbidden');
}
const { CallSid, From, To, CallStatus } = req.body;
console.log(`Incoming call from ${From}`);
// Log the call
const conn = await Datastore.open();
await conn.insertOne('calls', {
callSid: CallSid,
from: From,
to: To,
status: CallStatus,
direction: 'inbound',
receivedAt: new Date().toISOString(),
});
// Respond with TwiML for voice
res.set('Content-Type', 'text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="alice">Hello! Thank you for calling. Please leave a message after the beep.</Say>
<Record maxLength="60" transcribe="true" />
<Say>We did not receive a recording. Goodbye.</Say>
</Response>
`);
});
export default app.init();
Complete Multi-Event Handler
Handle all Twilio webhook types in one deployment:
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
// Middleware to validate all Twilio requests
function validateTwilioRequest(req, res, next) {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
console.error('Invalid Twilio signature');
return res.status(403).send('Forbidden');
}
next();
}
// Bypass JWT for all Twilio endpoints
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Incoming SMS
app.post('/twilio/sms', validateTwilioRequest, async (req, res) => {
const { From, Body, MessageSid } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'sms.received',
messageSid: MessageSid,
from: From,
body: Body,
timestamp: new Date().toISOString(),
});
// Auto-reply
res.set('Content-Type', 'text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks! We got your message.</Message>
</Response>
`);
});
// SMS Status Callback
app.post('/twilio/sms/status', validateTwilioRequest, async (req, res) => {
const { MessageSid, MessageStatus, ErrorCode } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'sms.status',
messageSid: MessageSid,
status: MessageStatus,
errorCode: ErrorCode,
timestamp: new Date().toISOString(),
});
res.status(200).send('OK');
});
// Incoming Voice Call
app.post('/twilio/voice', validateTwilioRequest, async (req, res) => {
const { CallSid, From, To } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'call.received',
callSid: CallSid,
from: From,
to: To,
timestamp: new Date().toISOString(),
});
res.set('Content-Type', 'text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="alice">Hello, thank you for calling!</Say>
<Hangup />
</Response>
`);
});
// Voice Status Callback
app.post('/twilio/voice/status', validateTwilioRequest, async (req, res) => {
const { CallSid, CallStatus, CallDuration } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'call.status',
callSid: CallSid,
status: CallStatus,
duration: CallDuration,
timestamp: new Date().toISOString(),
});
res.status(200).send('OK');
});
export default app.init();
Twilio Signature Verification
Twilio signs all webhook requests with the X-Twilio-Signature header using HMAC-SHA1 and your Auth Token. Always verify this signature to ensure requests are legitimately from Twilio.
import twilio from 'twilio';
const { validateRequest } = twilio;
function verifyTwilioWebhook(req) {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
// IMPORTANT: Use the exact URL Twilio is calling
// Include the full URL with https:// and any query parameters
const url = `https://${req.headers.host}${req.originalUrl}`;
return validateRequest(authToken, signature, url, req.body);
}
Important: Always use Twilio's official SDK for signature validation rather than implementing your own. The SDK handles edge cases like URL encoding and parameter ordering.
Twilio Webhook Timeouts
Twilio enforces a 15-second timeout. If your endpoint doesn't respond in time, Twilio marks the delivery as failed and may retry.
Best practice: Acknowledge immediately, process asynchronously:
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
// Define a worker for async processing
app.worker('processSms', async (req, res) => {
const { from, body, messageSid } = req.body.payload;
// Do heavy processing here (AI analysis, external API calls, etc.)
console.log(`Processing message ${messageSid} from ${from}`);
// Update database with results
const conn = await Datastore.open();
await conn.updateOne('sms_messages', { messageSid }, {
$set: {
processed: true,
processedAt: new Date().toISOString(),
}
});
res.end();
});
app.auth('/twilio/*', (req, res, next) => {
next();
});
app.post('/twilio/sms', async (req, res) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
if (!validateRequest(authToken, signature, url, req.body)) {
return res.status(403).send('Forbidden');
}
const { From, Body, MessageSid } = req.body;
// Store immediately
const conn = await Datastore.open();
await conn.insertOne('sms_messages', {
messageSid: MessageSid,
from: From,
body: Body,
processed: false,
receivedAt: new Date().toISOString(),
});
// Queue for async processing
await conn.enqueue('processSms', {
from: From,
body: Body,
messageSid: MessageSid,
});
// Respond immediately with TwiML (within milliseconds)
res.set('Content-Type', 'text/xml');
res.send(`
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Got it! Processing your request...</Message>
</Response>
`);
});
export default app.init();
Best Practices for Twilio Webhooks
1. Always Validate Signatures
Never skip signature validation in production. Attackers can send fake webhooks to your endpoint.
2. Use Environment Variables
coho set-env TWILIO_AUTH_TOKEN ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --encrypted
coho set-env TWILIO_ACCOUNT_SID your_account_sid --encrypted
3. Handle Idempotency
Twilio may retry failed webhooks. Use MessageSid or CallSid as idempotency keys:
const conn = await Datastore.open();
const existing = await conn.getOne('sms_messages', { messageSid: MessageSid });
if (existing) {
console.log('Already processed:', MessageSid);
res.status(200).send('OK');
return;
}
4. Return Proper Status Codes
200-299: Success, Twilio won't retry4xx/5xx: Twilio will retry with exponential backoff
5. Log Everything
console.log('Twilio webhook received:', {
type: 'sms',
from: From,
messageSid: MessageSid,
timestamp: new Date().toISOString(),
});
Check logs with: coho logs --follow
Why Use Codehooks.io for Twilio Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---|---|---|
| Setup time | Hours | 5 minutes |
| SSL/HTTPS | Configure certificates | Built-in |
| Signature validation | Manual implementation | Use Twilio SDK |
| Database for logs | Set up separately | Built-in |
| Async processing | Configure queues | Built-in workers |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs | Free tier available |
Related Resources
- Twilio Webhooks Documentation
- Twilio Webhook Security
- TwiML Reference
- Codehooks.io Quick Start
- Webhook Delivery System - Build your own outgoing webhook system
- What are Webhooks? - Learn webhook fundamentals
- Stripe Webhooks Example - Payment webhook handler
- Slack Webhooks Example - Another messaging integration
- Browse All Examples - Explore more templates and integrations
Twilio Webhooks FAQ
Common questions about handling Twilio webhooks
How do I verify Twilio webhook signatures?
validateRequest function from the official Twilio SDK. It takes your Auth Token, the X-Twilio-Signature header, the full webhook URL, and the request body. Always use the SDK rather than implementing your own validation to handle edge cases correctly.Why is my Twilio signature validation failing?
req.headers.host and req.originalUrl.How long do I have to respond to Twilio webhooks?
What's the difference between webhook and status callback?
or ) because Twilio needs to know how to handle the interaction. Status callbacks are informational - they report what happened (delivered, failed, etc.) and just need a 200 OK response.How do I test Twilio webhooks locally?
coho deploy and you have a live HTTPS URL in seconds. Configure that URL in the Twilio Console and test with real messages. Check logs with coho logs --follow.Does Twilio retry failed webhooks?
How do I handle MMS messages with media?
NumMedia (count of attachments) and MediaUrl0, MediaUrl1, etc. for each attachment. Download and store media files as needed. The MediaContentType0 parameter tells you the MIME type of each file.What format does Twilio send webhook data in?
application/x-www-form-urlencoded (not JSON). Most frameworks parse this automatically into req.body. The Codehooks.io runtime handles this parsing for you.