Twilio signs every webhook. It takes the exact URL it called (scheme, host, port, path and query string). For a form POST, it then adds every form parameter, sorted alphabetically by name, with each name and value appended and no delimiters. It runs HMAC-SHA1 over that string with your Auth Token as the key, then base64-encodes the result into X-Twilio-Signature. Use the SDK's validator, and make sure you give it the same URL and parameters Twilio used.
http:// and an internal host, while Twilio signed https://your-domain/.... Rebuild the public URL, for example from X-Forwarded-Proto and X-Forwarded-Host if you trust your proxy, or hard-code it.params argument. Pass only the POST form fields there.bodySHA256 query parameter, and the SDKs have a separate method that validates the raw body against it. Don't pass parsed JSON as form params.const twilio = require('twilio');
app.post('/sms', express.urlencoded({ extended: false }), (req, res) => {
const proto = req.get('x-forwarded-proto') || req.protocol; // only trust headers from your own proxy
const host = req.get('x-forwarded-host') || req.get('host');
const url = `${proto}://${host}${req.originalUrl}`;
const ok = twilio.validateRequest(process.env.TWILIO_AUTH_TOKEN, req.get('X-Twilio-Signature') || '', url, req.body);
if (!ok) return res.sendStatus(403);
res.type('text/xml').send('<Response/>');
});
In Express you can also use the twilio.webhook() middleware. With app.set('trust proxy', true), req.protocol reflects X-Forwarded-Proto.
from twilio.request_validator import RequestValidator
v = RequestValidator(os.environ['TWILIO_AUTH_TOKEN'])
url = 'https://your-domain.com' + request.full_path.rstrip('?') # the public URL Twilio called
if not v.validate(url, request.form, request.headers.get('X-Twilio-Signature', '')): abort(403)
The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. It verifies X-Twilio-Signature out of the box, for both form and JSON (bodySHA256) webhooks. Set SCHEME_<ID>=twilio. Use it for Twilio (status callbacks). Voice and SMS webhooks that need a TwiML reply can't go through the relay. It works the same way for Stripe, Lemon Squeezy, Shopify, Paddle Billing, Square and GitHub.
Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.
Free download: Webhook debugging cheat sheet.
Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint.