Skip to content

Verify Twilio webhooks

Verify that a webhook really came from Twilio before acting on it.

Identifier 'twilio'
Import import { verifyTwilio } from 'verihook/twilio'
Headers x-twilio-signature
Signature HMAC-SHA1 of the full public URL followed by the sorted form fields, base64-encoded. JSON bodies are covered by a bodySHA256 query parameter in the signed URL.
Replay window None (the provider sends no timestamp)
result.eventType none (Twilio callbacks have no event name)

Your account’s Auth Token: in the Twilio Console, open Account → API keys & tokens. Use the primary token unless you are rotating.

Store it in an environment variable (TWILIO_AUTH_TOKEN below) and never commit it.

import { verifyTwilio } from 'verihook/twilio';
export async function POST(request: Request) {
const result = await verifyTwilio(request, process.env.TWILIO_AUTH_TOKEN!);
if (!result.valid) {
console.warn(result.code, result.reason, result.hint);
return new Response('Invalid signature', { status: 401 });
}
console.log(result.eventType, result.event);
return Response.json({ received: true });
}

request can be a Fetch Request or { headers, body, url? } with the raw body. verifyWebhook('twilio', request, secret) from verihook does the same. Read why the raw body matters if verification fails behind a body parser.

app/api/webhooks/twilio/route.ts
import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('twilio', process.env.TWILIO_AUTH_TOKEN!, async (payload, result) => {
// Runs only for a valid signature
console.log(result.eventType, result.event);
});

Each adapter responds 401 to an invalid signature, 413 to a body over maxBodySize (2 MB by default) and 200 to a duplicate when you pass a dedupeStore.

  • Twilio signs the public URL it called, including the query string. Behind a proxy or tunnel (ngrok, a load balancer), verihook rebuilds it from x-forwarded-proto and x-forwarded-host. If those aren’t forwarded, pass { url: 'https://your.public/url' }.
  • result.event holds the form fields (Body, From, To, …).

signWebhook from verihook/testing builds a correctly signed Twilio request for your tests:

import { signWebhook } from 'verihook/testing';
import { verifyTwilio } from 'verihook/twilio';
const hook = await signWebhook('twilio', {
secret: 'twilio_auth_token',
payload: { id: 'evt_test' },
});
const result = await verifyTwilio(
{ headers: hook.headers, body: hook.body, url: hook.url },
hook.secret,
);
// result.valid === true

From the command line, npx verihook simulate twilio --url http://localhost:3000/webhooks/twilio sends one to your local server.

See Testing your handlers for supertest and Fetch Request examples.

Failed results carry a code and often a hint with the likely cause. See Troubleshooting for each error code.

Twilio’s webhook documentation