Twilio Webhooks Integration 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();