How to Test Twilio Webhooks with HookCap
<h1> How to Test Twilio Webhooks with HookCap </h1> <p>Twilio sends webhooks for every significant event in its platform: incoming SMS messages, voice call status changes, delivery receipts, WhatsApp messages, and more. If your app responds to any of these, you need a reliable way to capture and inspect real payloads during development.</p> <p>The core problem is the same as every webhook integration: Twilio needs a public HTTPS URL, but your handler is on <code>localhost</code>. This guide covers using HookCap to solve that during development.</p> <h2> What Twilio Webhooks Are Used For </h2> <p>Twilio webhooks let your server react to events from the Twilio platform:</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Webhook type</th> <th>When it fires</th> </tr> </thead> <
How to Test Twilio Webhooks with HookCap
Twilio sends webhooks for every significant event in its platform: incoming SMS messages, voice call status changes, delivery receipts, WhatsApp messages, and more. If your app responds to any of these, you need a reliable way to capture and inspect real payloads during development.
The core problem is the same as every webhook integration: Twilio needs a public HTTPS URL, but your handler is on localhost. This guide covers using HookCap to solve that during development.
What Twilio Webhooks Are Used For
Twilio webhooks let your server react to events from the Twilio platform:
Webhook type When it fires
Incoming SMS A message arrives on your Twilio number
SMS Status Callback Delivery status changes (sent, delivered, failed)
Incoming voice call Someone calls your Twilio number
Call Status Callback A call's state changes (initiated, ringing, answered, completed)
WhatsApp messages Incoming WhatsApp messages on your number
Verify service events OTP code sent, check attempts, etc.
Each webhook is an HTTP request Twilio sends to a URL you configure, either in the Twilio Console or via the API.
Step 1: Create a HookCap Endpoint
Go to hookcap.dev, sign up, and create an endpoint. You get a persistent HTTPS URL like:
https://hookcap.dev/e/your-endpoint-id
Enter fullscreen mode
Exit fullscreen mode
This URL works immediately — no local server required. Twilio can reach it from anywhere.
Step 2: Configure Twilio to Send to HookCap
For SMS (Messaging Service or Phone Number)
In the Twilio Console:
-
Go to Phone Numbers → Active Numbers
-
Select the number you want to configure
-
Under Messaging Configuration, set the Incoming Message webhook URL to your HookCap endpoint
-
Set the method to HTTP POST
-
Save
Alternatively, configure via the Twilio API:
const twilio = require('twilio'); const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);const twilio = require('twilio'); const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);await client.incomingPhoneNumbers(process.env.TWILIO_PHONE_NUMBER_SID) .update({ smsUrl: 'https://hookcap.dev/e/your-endpoint-id', smsMethod: 'POST', });`
Enter fullscreen mode
Exit fullscreen mode
For Status Callbacks
Status callbacks are configured per-message when you send:
const message = await client.messages.create({ body: 'Hello from Twilio', from: process.env.TWILIO_PHONE_NUMBER, to: '+1234567890', statusCallback: 'https://hookcap.dev/e/your-endpoint-id', });const message = await client.messages.create({ body: 'Hello from Twilio', from: process.env.TWILIO_PHONE_NUMBER, to: '+1234567890', statusCallback: 'https://hookcap.dev/e/your-endpoint-id', });Enter fullscreen mode
Exit fullscreen mode
For Voice
In the Twilio Console, go to Phone Numbers → Active Numbers → select your number, then configure the Voice webhook URL.
Step 3: Trigger Events and Inspect Payloads
Send a message to your Twilio number (or call it). HookCap captures the webhook delivery and displays it in real time.
A typical incoming SMS webhook from Twilio looks like:
POST https://hookcap.dev/e/your-endpoint-id
Headers: Content-Type: application/x-www-form-urlencoded I-Twilio-Signature: AbCdEfGhIjKlMnOpQrStUvWxYz= X-Forwarded-For: 3.88.0.0
Body (form-encoded): ToCountry=US ToState=CA SmsMessageSid=SM1234567890abcdef1234567890abcdef NumMedia=0 ToCity=SAN FRANCISCO FromZip=10001 SmsSid=SM1234567890abcdef1234567890abcdef FromState=NY SmsStatus=received FromCity=NEW YORK Body=Hello world FromCountry=US To=+14155551234 ToZip=94102 NumSegments=1 MessageSid=SM1234567890abcdef1234567890abcdef AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx From=+12125551234 ApiVersion=2010-04-01`
Enter fullscreen mode
Exit fullscreen mode
Note that Twilio webhooks are form-encoded, not JSON. This matters for signature verification.
A status callback looks different — it includes the delivery status:
MessageSid=SM1234567890abcdef MessageStatus=delivered ErrorCode= AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxMessageSid=SM1234567890abcdef MessageStatus=delivered ErrorCode= AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxEnter fullscreen mode
Exit fullscreen mode
Step 4: Verify the Twilio Signature
Twilio uses a different signature scheme from Stripe and GitHub. Instead of signing just the body, Twilio signs the full request URL plus all POST parameters.
The algorithm:
-
Take the full URL (including query string)
-
Append each POST parameter (key + value) in alphabetical order
-
Sign the result with your Auth Token using HMAC-SHA1
-
Base64-encode the result
const crypto = require('crypto');
function validateTwilioSignature(authToken, signature, url, params) {
// Sort params alphabetically and concatenate key+value
const sortedParams = Object.keys(params)
.sort()
.map(key => ${key}${params[key]})
.join('');
const stringToSign = url + sortedParams;
const expectedSig = crypto .createHmac('sha1', authToken) .update(Buffer.from(stringToSign, 'utf-8')) .digest('base64');
// Constant-time comparison return crypto.timingSafeEqual( Buffer.from(expectedSig), Buffer.from(signature) ); }
// In your Express handler
app.post('/webhook/twilio/sms', express.urlencoded({ extended: false }), (req, res) => {
const signature = req.headers['x-twilio-signature'];
const url = ${req.protocol}://${req.get('host')}${req.originalUrl};
if (!validateTwilioSignature( process.env.TWILIO_AUTH_TOKEN, signature, url, req.body )) { return res.status(403).send('Forbidden'); }
const incomingSms = req.body;
console.log(SMS from ${incomingSms.From}: ${incomingSms.Body});
// Respond with TwiML const twiml = new twilio.twiml.MessagingResponse(); twiml.message('Got it!'); res.type('text/xml'); res.send(twiml.toString()); });`
Enter fullscreen mode
Exit fullscreen mode
Or use the official Twilio helper library:
const twilio = require('twilio');
app.post('/webhook/twilio', express.urlencoded({ extended: false }), (req, res) => {
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN,
req.headers['x-twilio-signature'],
https://yourdomain.com${req.path}, // must be the exact URL Twilio used
req.body
);
if (!isValid) { return res.status(403).send('Forbidden'); }
// Handle the webhook... });`
Enter fullscreen mode
Exit fullscreen mode
Important: The URL Must Match Exactly
Twilio signature verification is sensitive to the URL. If Twilio called https://yourdomain.com/webhook/sms but you reconstruct it as http://yourdomain.com/webhook/sms (wrong protocol) or https://yourdomain.com/webhook/sms?foo=bar (extra query param), verification will fail.
Step 5: Use HookCap Auto-Forward to Test Locally (Pro)
With HookCap's Auto-Forward feature, you can forward captured webhooks to your local server — no tunnel setup needed.
-
In your HookCap dashboard, enable Auto-Forward on your endpoint
-
Set the forward URL to http://localhost:3000/webhook/twilio
-
HookCap proxies incoming webhooks from Twilio to your local server in real time
This is more stable than ngrok or localtunnel for Twilio development because:
-
The HookCap URL stays constant (no need to reconfigure Twilio each time)
-
You can see both the raw Twilio payload AND your server's response in the HookCap dashboard
-
If your local server is down, HookCap still captures the webhook for later replay
Common Twilio Webhook Issues
Signature Verification Fails Behind a Proxy
If your server sits behind a load balancer or reverse proxy, the URL your app sees may not match the URL Twilio used. The host, protocol, or port might differ. Fix this by explicitly reconstructing the URL:
// In Express with a trusted proxy app.set('trust proxy', 1); const url = // In Express with a trusted proxy app.set('trust proxy', 1); const url = ;Enter fullscreen mode
Exit fullscreen mode
Or just hardcode the production webhook URL:
const WEBHOOK_URL = 'https://yourdomain.com/webhook/twilio'; const isValid = twilio.validateRequest(authToken, sig, WEBHOOK_URL, req.body);const WEBHOOK_URL = 'https://yourdomain.com/webhook/twilio'; const isValid = twilio.validateRequest(authToken, sig, WEBHOOK_URL, req.body);Enter fullscreen mode
Exit fullscreen mode
Form Encoding vs JSON
Twilio webhooks are application/x-www-form-urlencoded, not JSON. Make sure your framework parses them correctly:
// Correct: use urlencoded parser app.post('/webhook/twilio', express.urlencoded({ extended: false }), handler);// Correct: use urlencoded parser app.post('/webhook/twilio', express.urlencoded({ extended: false }), handler);// Wrong: json parser won't parse Twilio's form-encoded body app.post('/webhook/twilio', express.json(), handler);`
Enter fullscreen mode
Exit fullscreen mode
No Response TwiML
For incoming SMS and voice webhooks, Twilio expects a TwiML response. If you return JSON or plain text, Twilio will log an error (though your handler still "worked"). Return valid TwiML:
const twiml = new twilio.twiml.MessagingResponse(); twiml.message('Thank you for your message'); res.type('text/xml').send(twiml.toString());const twiml = new twilio.twiml.MessagingResponse(); twiml.message('Thank you for your message'); res.type('text/xml').send(twiml.toString());Enter fullscreen mode
Exit fullscreen mode
For status callbacks, a plain 200 OK with no body is fine.
Debugging Workflow
-
Capture the raw payload in HookCap — See exactly what Twilio sent, including all headers and the form-encoded body
-
Check the SmsStatus or MessageStatus field — Know what state Twilio thinks the message is in
-
Replay to your local handler — Use HookCap replay to send the exact captured payload to localhost
-
Check the Twilio Console error logs — Under Monitor → Logs → Errors, Twilio shows delivery failures with reason codes
HookCap captures the full request including the X-Twilio-Signature header, which you can use to understand what URL Twilio is signing and debug verification failures.
Summary
Testing Twilio webhooks with HookCap:
-
Create a HookCap endpoint and set it as your Twilio webhook URL
-
Trigger real events (send SMS, make calls, or use the Twilio Console "Test" feature)
-
Inspect the form-encoded payload and headers in HookCap
-
Note: Twilio signs URL + sorted params (not just body) — use the Twilio SDK's validation helper
-
Use Auto-Forward to proxy live Twilio webhooks to your local server for integrated testing
Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
versionupdateproduct
Resolve.ai Alternative: Open Source AI for Incident Investigation
Key Takeaway: Resolve.ai is a $1B-valued AI SRE platform used by Coinbase, DoorDash, and Salesforce — but pricing requires contacting sales with no public pricing page. Aurora is an open source (Apache 2.0) alternative that delivers autonomous AI investigation with sandboxed cloud execution, infrastructure graphs, and knowledge base search — completely free and self-hosted. What is Resolve.ai? Resolve.ai is an AI-powered autonomous SRE platform founded in 2024 by Spiros Xanthos (former SVP at Splunk, co-creator of OpenTelemetry ) and Mayank Agarwal. It raised $125M in Series A at a reported $1 billion valuation , backed by Lightspeed and Greylock with angels including Fei-Fei Li and Jeff Dean. Resolve.ai positions as "machines on call for humans" — a multi-agent AI system that autonomously

The Agent Data Layer: A Missing Layer in AI Architecture
AI agents are getting access to production data and we’re doing it wrong. Most teams are connecting agents directly to databases. This works in demos. It breaks in production. Because AI agents are not deterministic systems. They: explore instead of follow rules generate queries instead of executing predefined logic optimize for answers, not safety Databases were built for humans. Agents don’t understand consequences. What actually goes wrong When you connect an agent directly to a database, you introduce a new class of failures: Unpredictable queries Full table scans Schema exposure Cross-tenant data leaks Destructive operations on production A simple prompt like: "Show me recent orders" can turn into: SELECT * FROM orders JOIN customers ON ... JOIN payments ON ... Now you’ve exposed ever
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products

From MOUs to Markets: Transatlantic Deals Face Reality Test
Why transatlantic execution, not transatlantic symbolism, now matters to the electronics and semiconductor supply chain. The post From MOUs to Markets: Transatlantic Deals Face Reality Test appeared first on EE Times . ]]>

RLDatix's Connected Healthcare Summit Draws 400+ Health System Leaders as Company Advances AI-Powered Patient Safety and Provider Performance Solutions - PR Newswire
RLDatix's Connected Healthcare Summit Draws 400+ Health System Leaders as Company Advances AI-Powered Patient Safety and Provider Performance Solutions PR Newswire





Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!