Twilio Webhooks: Setup, Payloads, Signature Validation & Testing

A complete guide to Twilio webhooks — how they work, how to configure them for SMS and voice, the form-encoded payload, validating the X-Twilio-Signature header, retry behavior, and how to test Twilio webhooks on localhost.

Twilio uses webhooks for almost everything reactive: an incoming SMS or MMS, an inbound call, and status updates as your messages and calls move through the network. This guide covers how Twilio webhooks work, how to configure and secure them, and how to test them without deploying. (New to webhooks generally? Start with what is a webhook.)

How Twilio webhooks work

When something happens, Twilio makes an HTTP request to a URL you control. By default it's a POST, and — importantly — the body is application/x-www-form-urlencoded, not JSON. Twilio expects a quick response: TwiML (XML) for messaging and voice so it knows what to do next, or a plain 2xx for status callbacks.

There are three broad categories:

  • Incoming messages — someone texts your Twilio number; Twilio POSTs the message to your Messaging webhook and uses your TwiML reply to respond.
  • Incoming voice — an inbound call hits your number; Twilio fetches TwiML from your Voice webhook.
  • Status callbacks — as a message or call changes state (queuedsentdelivered/failed), Twilio POSTs updates to the StatusCallback URL you set.

Configuring a Twilio webhook

You set webhook URLs in a few places depending on the product:

  1. Per phone number — Console → Phone Numbers → Manage → Active numbers → your number → Messaging / Voice "A message/call comes in" webhook.
  2. Messaging Service — Console → Messaging → Services → your service → Integration.
  3. Per API request — pass a StatusCallback URL when you create a message or call to receive delivery status.

The Twilio webhook payload

Because it's form-encoded, you parse parameters, not JSON. A typical inbound SMS looks like:

POST /twilio HTTP/1.1
Content-Type: application/x-www-form-urlencoded
X-Twilio-Signature: hZ8...=

MessageSid=SM123&From=%2B14155551212&To=%2B14155557890&Body=Hello&NumMedia=0

Common fields include MessageSid, From, To, Body, NumMedia (messaging) and CallSid, CallStatus, MessageStatus (voice / status callbacks). Parse the body with your framework's URL-encoded form parser — for example express.urlencoded() in Node or request.form in Flask.

Responding with TwiML

For messaging and voice, reply with TwiML telling Twilio what to do:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Message>Thanks, we got your text!</Message>
</Response>

For status callbacks there's nothing to instruct, so just return 204/200.

Validating the X-Twilio-Signature

Every Twilio request carries an X-Twilio-Signature header so you can confirm it really came from Twilio. It's an HMAC-SHA1 of the full request URL plus the alphabetically sorted POST parameters, keyed with your Auth Token, then Base64-encoded. Recompute it and compare — most people use Twilio's RequestValidator helper.

The classic gotcha: the URL you validate against must be the exact public URL Twilio called (scheme, host, path and query string). Behind a proxy or tunnel, the host your app sees may differ from the public host — validate against the public one. See how to verify a webhook signature, try the Twilio signature verifier, or use the free HMAC verifier.

Retries and reliability

Twilio's retry behavior varies by product and is generally limited — if your endpoint is slow or returns an error, you can lose the notification (verify Twilio's current behavior for your specific webhook type). For anything business-critical, put a webhook gateway in front of your handler: it verifies the signature, persists each event and applies durable retries so a deploy or a brief outage doesn't drop a delivery, and it keeps delivery logs you can inspect and replay.

Testing Twilio webhooks locally

You don't want to deploy just to see what Twilio sends. Two options:

  1. Inspect first (no install): open a free Webhook Bin, paste its URL into your Twilio number's webhook field, and text the number — you'll see the exact form-encoded request and headers. See the Twilio webhook tester.
  2. Forward to localhost: point Twilio at a stable Webhook Relay URL and run the agent so requests reach your machine with no public IP:
relay forward --bucket twilio-dev http://localhost:3000/twilio

Full walkthrough: receive Twilio webhooks on localhost.

Common issues

  • Empty body / can't read fields — you're parsing JSON; Twilio sends form-encoded data. Use a URL-encoded parser.
  • Signature always fails — the URL you validate doesn't match the public URL Twilio called (often a proxy/tunnel host or a missing query string).
  • Twilio shows an 11200 error — your endpoint didn't return a valid response (or valid TwiML) quickly enough; acknowledge fast and do heavy work asynchronously.

Next steps

Ready to see your first Twilio webhook? Open a free Webhook Bin or start forwarding to localhost for free.