Providers / April 25, 2026

How to receive Twilio webhooks (SMS, voice, status callbacks)

Twilio webhook flow showing SMS, voice, and status-callback delivery with fallback URL

Twilio fires webhooks for incoming SMS messages, voice call events (inbound, ringing, answered, completed), and message status callbacks (queued, sent, delivered, failed). They're how integrations stay in sync with what's happening in your Twilio account.

Twilio webhooks behave differently from most other providers — payloads are form-urlencoded (not JSON), retries don't happen by default for messaging, and voice webhooks expect a TwiML response. This guide covers what Twilio sends, how to verify it, and what production reliability requires.

What Twilio sends

Twilio webhooks are POST requests with application/x-www-form-urlencoded bodies (not JSON, unlike most other providers). An incoming SMS:

ToCountry=US
ToState=CA
SmsMessageSid=SM1234567890abcdef
NumMedia=0
FromZip=94016
SmsSid=SM1234567890abcdef
FromState=CA
SmsStatus=received
FromCity=SAN+FRANCISCO
Body=Hello+from+Twilio
FromCountry=US
To=%2B14155551234
NumSegments=1
MessageSid=SM1234567890abcdef
AccountSid=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From=%2B14155555678
ApiVersion=2010-04-01

Headers include:

X-Twilio-Signature: <base64 signature>
Content-Type: application/x-www-form-urlencoded

There are dozens of webhook event shapes — each Twilio product (Messaging, Voice, Studio, Conversations, Verify, Video) sends its own.

Twilio's signature scheme

The signature is HMAC-SHA1 (yes, SHA1 — it's legacy) of the full request URL plus the sorted form parameters concatenated into a string, signed with your account auth token, base64-encoded:

signature = base64(hmac_sha1(auth_token, url + sorted_form_param_string))

Where sorted_form_param_string is form parameters sorted alphabetically by key, then concatenated as key1value1key2value2....

Verification:

  1. Reconstruct the full URL Twilio called (including query string)
  2. Read the form parameters
  3. Sort them alphabetically by key
  4. Concatenate as described
  5. Compute HMAC-SHA1 with your auth token
  6. Base64-encode and compare constant-time with X-Twilio-Signature

Twilio's official SDKs (twilio for Node, twilio for Python, etc.) ship a verifier helper. Use it.

Twilio's retry policy (or lack of one)

This is where Twilio diverges from Stripe and GitHub:

  • Messaging webhooks (incoming SMS, MMS): If your URL returns non-2xx, Twilio does NOT retry. The message is dropped from your handler's perspective. Twilio supports a fallback URL that gets called if the primary URL fails — configure both.
  • Voice webhooks: Same — no automatic retry. Configure a voice fallback URL.
  • Status callbacks (MessageStatus, CallStatus): These DO retry, up to a few times with backoff.

The "no retry on messaging" behavior is the trap. A 30-second outage can lose every inbound SMS that arrived during it, with no automatic recovery.

This means:

  • Acknowledge fast. Messaging webhooks have a ~15s response window; voice has ~10s.
  • Configure fallback URLs. Twilio's primary/fallback pattern is the only built-in retry for messaging.
  • For voice, return TwiML. Voice webhooks expect TwiML (XML) back describing what to do next on the call. An empty or malformed response ends the call.
  • Have a recovery story for dropped messaging. Without retries, a downed handler equals lost messages — unless you put a relay in front.

DIY: minimal Twilio webhook handler in Node

import twilio from 'twilio'

const authToken = process.env.TWILIO_AUTH_TOKEN!

export async function POST(req: Request) {
  const sig = req.headers.get('x-twilio-signature')!
  const url = `https://${req.headers.get('host')}${new URL(req.url).pathname}`
  const body = await req.text()
  const params = Object.fromEntries(new URLSearchParams(body))

  const isValid = twilio.validateRequest(authToken, sig, url, params)
  if (!isValid) return new Response('Bad signature', { status: 403 })

  // Idempotency on MessageSid (or CallSid for voice)
  // ...

  queueWork(params)

  // For SMS: an empty 200 is fine
  return new Response('', { status: 200 })

  // For voice: return TwiML, e.g.:
  // return new Response('<Response><Say>Hello</Say></Response>', {
  //   headers: { 'Content-Type': 'text/xml' },
  // })
}

Production needs more:

  • A persistence layer for MessageSid / CallSid idempotency
  • A fallback URL configured (or a relay holding events)
  • Replay path for dropped messages — requires the relay since Twilio doesn't retry
  • Filtering — production accounts can see thousands of status-callback events per day
  • Multiple Twilio numbers or accounts in a multi-tenant app
  • TwiML response generation for voice flows

Hooksbase: receive Twilio webhooks without rebuilding the rest

Twilio isn't one of Hooksbase's five pre-verified provider packs (Stripe, GitHub, Clerk, Slack, Resend), and Hooksbase does not forward Twilio's original signature header to your destination. If you need Twilio request validation, verify it in a small pre-ingest forwarder, then post the verified form body to Hooksbase with the bearer ingest secret.

Setup:

  1. Create a webhook in Hooksbase with your application URL as the destination
  2. Configure Twilio to call your verification forwarder as the primary webhook URL
  3. In the forwarder, validate X-Twilio-Signature against the exact URL Twilio called, then POST the same form body to the Hooksbase ingest URL with Authorization: Bearer <ingest secret>

You get:

  • Acknowledge in milliseconds to Twilio, well within the 10-15 second budget
  • Idempotency for your destination — every dispatch includes a unique webhook-id header (Standard Webhooks-compatible) you can dedupe on. Twilio's MessageSid is forwarded in the body for downstream use.
  • Retries to your endpoint — Hooksbase keeps retrying until your handler succeeds, even though Twilio itself doesn't retry messaging
  • Routing rules by Twilio event shape — send incoming SMS to one destination, status callbacks to another
  • Payload transforms — flatten Twilio's form-encoded structure into JSON if your downstream expects JSON
  • Deterministic replay — re-run a failed delivery with the same payload bytes while the payload is retained
  • Delivery history and DLQ

Common Twilio webhook use cases for AI agents

  • Inbound SMS triage agent — incoming SMS triggers an agent that interprets the message, looks up the customer, and either replies via the Twilio API or hands off to a human.
  • Voice IVR agent — incoming call triggers an agent that drives the IVR flow via TwiML, escalating to a human for off-script requests.
  • Delivery confirmation agentMessageStatus: failed triggers an agent that retries the send with a different number or escalates.
  • Verification flow agentVerify webhooks trigger an agent that completes downstream onboarding steps once a phone number is confirmed.
  • Call summary agentCallStatus: completed triggers an agent that pulls the recording, transcribes it, and summarizes into the CRM.

Where to go next

Start free at app.hooksbase.com.

Related guides