Twilio
Twilio signs your public endpoint URL plus the sorted params — not the body — so the URL has to be right.
Twilio signs each request with HMAC-SHA1, base64, over your
endpoint’s full public URL plus the sorted request parameters, in
X-Twilio-Signature (scheme family 7 — the
canonical-string family). Nothing time-varying is signed, so pair it with an
idempotency store.
import { createWebhookHandler, memoryIdempotencyStore } from 'webhooks-sdk'
import { twilio } from 'webhooks-sdk/twilio'
const handler = createWebhookHandler({
provider: twilio({
authToken: process.env.TWILIO_AUTH_TOKEN!,
url: 'https://example.com/webhooks/twilio',
}),
idempotency: memoryIdempotencyStore(),
on: {
'message.received': async (event) => await reply(event.payload),
'call.completed': async (event) => await logCall(event.payload),
},
})
export const POST = handler.fetch
Options
| Option | Type | Default | |
|---|---|---|---|
authToken |
string | string[] |
— | Your account’s auth token. Pass [primary, secondary] while rotating. |
url |
string | ((raw) => string) |
the request URL | The exact URL Twilio signs — see below. |
The URL is part of the signature
Twilio signs the URL as configured in the Twilio console, including
scheme, host, and query string. The default — using the incoming request’s
own URL — is only correct when nothing rewrites it. Behind a load balancer,
a tunnel, or a framework rewrite, set url explicitly:
// Static:
twilio({ authToken, url: 'https://example.com/webhooks/twilio' })
// Multi-tenant — derive it per request:
twilio({ authToken, url: (raw) => tenantUrl(raw.header('host')) })
A string url without a query string adopts the request’s own query
parameters, since Twilio appends signed params (like bodySHA256) to your
configured URL.
Form and JSON deliveries
Classic Twilio webhooks are form-encoded: the signature covers the URL plus
the sorted form fields, and event.payload is the field record. For
JSON-bodied products, Twilio instead appends a bodySHA256 parameter to
the URL and signs that — the SDK verifies the body against it as part of
the same signature check.
The envelope
event.type—EventTypeverbatim where a product sends one (Conversations, TaskRouter, Sync); otherwise derived asmessage.<status>/call.<status>fromMessageStatus,SmsStatus, orCallStatus.event.id— built from theMessageSid/CallSidplus the event type, so status callbacks for the same message deduplicate per status, not per message.event.timestamp— receipt time; nothing on the wire.
Standalone & testing
import {
verifyTwilioWebhook, // (raw, { authToken, url? }) — throws on failure
parseTwilioWebhook, // (raw) — the envelope
signTwilioWebhook, // (url, params, authToken) — a valid header value, for tests
} from 'webhooks-sdk/twilio'
See Standalone verification and Testing.