All Webhook Errors

Twilio Webhook Signature Validation Failed — Causes & Fixes

Twilio's webhook signature scheme is fundamentally different from Stripe / GitHub / Shopify — it does not sign the request body. Instead, it signs the full request URL plus the sorted form parameters. This breaks more verification code than any other webhook authentication scheme.

How Twilio Signs Requests

Twilio's signature is computed from:

  1. The full URL the webhook was sent to (including any query string).
  2. For each form parameter, sorted alphabetically by name: append name + value to the URL string.
  3. HMAC-SHA1 the result with your auth token.
  4. Base64-encode the digest. Send as X-Twilio-Signature.

This means: changing any part of the URL or any parameter changes the expected signature. Most failures come from mismatches in what the receiver thinks the URL is vs what Twilio thinks it is.

Root Causes

1. URL mismatch from reverse proxy

Twilio sends to https://api.example.com/webhook. Your nginx rewrites to https://internal.example.com/webhook before reaching your handler. Your handler reconstructs the URL using req.protocol + req.host + req.url = internal.example.com. Twilio signed api.example.com. Mismatch.

2. HTTP vs HTTPS

Twilio signed https://... but your reverse proxy strips TLS and your handler sees http://. The protocol is part of the signed URL. Use X-Forwarded-Proto to reconstruct the original protocol.

3. Query string ordering

If your URL has query parameters (?account=abc&event=ring), Twilio's signature includes them in the URL string. Reordering them changes the signed input. Don't normalize the query string.

4. Form parameter handling

Twilio sends application/x-www-form-urlencoded, not JSON. Your handler must parse form params. The signature math iterates over them sorted by key. Use Twilio's official SDK for verification — manually implementing the algorithm is error-prone.

Fix It — Use Twilio's SDK

import express from 'express'
import twilio from 'twilio'

const app = express()
const authToken = process.env.TWILIO_AUTH_TOKEN

app.post('/webhooks/twilio',
  express.urlencoded({ extended: false }),
  (req, res) => {
    const sig = req.headers['x-twilio-signature']

    // Reconstruct the original URL Twilio signed
    const proto = req.headers['x-forwarded-proto'] || req.protocol
    const host = req.headers['x-forwarded-host'] || req.get('host')
    const url = \`\${proto}://\${host}\${req.originalUrl}\`

    const valid = twilio.validateRequest(authToken, sig, url, req.body)
    if (!valid) return res.status(401).send('invalid signature')

    res.set('Content-Type', 'text/xml').send('<Response/>')
  }
)

Twilio-Specific Gotchas

  • Twilio webhooks expect a TwiML response (<Response>...</Response>) for voice/SMS callbacks, not just 200. An empty <Response/> tells Twilio "no further action."
  • Twilio's signature uses HMAC-SHA1, not SHA256. SHA1 is otherwise deprecated; Twilio's docs explain why they kept it (legacy compatibility).
  • The auth token rotates: your account auth token (in Twilio Console) is the secret for all webhooks across the account. Rotating it requires updating every receiver simultaneously.
  • Twilio retries on 5xx; 4xx responses (including your 401) stop retries. Make sure 401 is what you want — it usually is, but verify before deploying.

How to Reproduce

In Twilio Console: Phone Numbers → click your number → click the webhook URL → "Test" button (some Console pages have this). The test fires a real signed webhook. If your handler rejects it, capture the request and reconstruct the URL Twilio actually sent vs what your handler thinks. The mismatch is the bug.

Frequently Asked Questions

Why does Twilio sign the URL instead of the body?

Historical: Twilio's signing scheme predates JSON webhooks. The form-encoded body could be any subset of voice/SMS params; signing the URL+params was the simplest scheme that covered all cases.

Can I use Twilio webhooks behind a load balancer?

Yes, but you must reconstruct the original URL using X-Forwarded-Proto and X-Forwarded-Host, not the internal URL your handler sees. Twilio's SDK handles this if you pass the right URL.

My Twilio webhook works for SMS but not voice. Why?

Voice webhooks include additional form parameters (CallSid, CallerName, etc.) that change the signed input. The verification math is identical — the SDK handles it. If verification fails for voice only, double-check you're using the right auth token (account-level, same for both).

Debug This Error in Real Time

WebhookWhisper captures every webhook request with full headers, body, and timing — so you can see exactly what the provider sent and reproduce the error instantly.

Start Debugging Free
Twilio Webhook Signature Validation Failed — Causes & Fixes (2026) | WebhookWhisper