Twilio Webhook: Example Payload, Headers and Signature

Twilio's webhooks are form posts, not JSON, and the signature covers your URL as well as the parameters. Here is an incoming-message request and how to validate it.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by Twilio, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

How the webhook URL works. It is a public address that records whatever is sent to it and answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is deleted 24 hours after it was created — or sooner if you press Delete URL or our server restarts, because requests are held in memory and never written to disk.

Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.

What is a Twilio webhook?

When a text message reaches one of your Twilio numbers, Twilio makes an HTTP request — POST by default — to the webhook URL configured on that number, and waits for instructions. The request body is application/x-www-form-urlencoded: a flat list of parameters such as From, To, Body and MessageSid.

Twilio sends two kinds of webhook and they behave differently. An incoming message or call webhook expects a reply telling Twilio what to do next. A status callback — delivery receipts, call progress — is purely informational and only needs a 2xx.

What a Twilio webhook request looks like

The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.

POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Twilio-Signature: RSOYDt4T1cUTdK1PDd93/VVr8B8=
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
I-Twilio-Idempotency-Token: 5b2d1f0e-9f6a-4a0b-9d3e-6f2c1a7b8c9d
User-Agent: TwilioProxy/1.1
ToCountry=US&ToState=CA&SmsMessageSid=SM00000000000000000000000000000001&NumMedia=0&ToCity=SAN+FRANCISCO&FromZip=10001&SmsSid=SM00000000000000000000000000000001&FromState=NY&SmsStatus=received&FromCity=NEW+YORK&Body=Is+my+order+on+the+way%3F&FromCountry=US&To=%2B14155550100&ToZip=94105&NumSegments=1&MessageSid=SM00000000000000000000000000000001&AccountSid=AC00000000000000000000000000000001&From=%2B12125550123&ApiVersion=2010-04-01

Decoded, that is a set of key/value pairs: From=+12125550123, To=+14155550100, Body=Is my order on the way?, MessageSid=SM…, NumMedia=0, and geographic guesses about both numbers. Media messages add MediaUrl0, MediaContentType0 and so on. Twilio adds parameters over time, so a handler must tolerate keys it has never seen.

Headers Twilio sends

HeaderExampleWhat it is
X-Twilio-SignatureRSOYDt4T1cUTdK1PDd93/VVr8B8=HMAC-SHA1, base64, computed over the full URL plus the sorted parameters.
Content-Typeapplication/x-www-form-urlencoded; charset=…Form encoding — the body is not JSON.
I-Twilio-Idempotency-Token5b2d1f0e-9f6a-4a0b-9d3e-6f2c1a7b8c9dIdentifies the webhook attempt so a retried request can be recognised.
User-AgentTwilioProxy/1.1Identifies the sender.

Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.

How Twilio signs a webhook

Twilio starts with the full URL it requested — scheme, host, path and query string, exactly as configured. For a form POST it then sorts the parameters by name and appends each name immediately followed by its value, with no separators. It signs that string with HMAC-SHA1 using your account's Auth Token and sends the base64 digest as X-Twilio-Signature.

Because the URL is part of the signed data, verification fails whenever your application reconstructs a different URL from the one Twilio called — most often behind a proxy or load balancer that terminates TLS, where the app sees http:// and an internal host. Build the URL from the public address, not from the socket. For JSON-bodied webhooks Twilio instead adds a bodySHA256 query parameter and signs the URL alone.

const crypto = require('crypto');

// url: the exact public URL Twilio requested, including any query string
// params: the parsed form fields as an object
function verifyTwilio(url, params, header, authToken) {
  const data = Object.keys(params).sort()
    .reduce((acc, key) => acc + key + params[key], url);
  const expected = crypto.createHmac('sha1', authToken)
    .update(Buffer.from(data, 'utf-8')).digest('base64');
  const a = Buffer.from(header || '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The response is the point

For an incoming message Twilio reads your response body as TwiML — XML such as <Response><Message>On its way!</Message></Response> — and acts on it. To do nothing, return an empty <Response/> or a 2xx with no body. A response Twilio cannot interpret is logged as an error in the console's debugger.

Twilio waits a limited number of seconds for that response, per its documentation at the time of writing, and does not retry an HTTP error by default; instead each number has an optional fallback URL that is requested when the primary one fails. Retry behaviour on connection failures can be tuned with connection overrides appended to the webhook URL.

Catch a real Twilio webhook

  1. Press Create webhook URL at the top of this page and copy the address.
  2. In the Twilio Console, open Phone Numbers → Manage → Active numbers and select a number.
  3. Under Messaging configuration, set "A message comes in" to Webhook, paste the address, keep HTTP POST and save.
  4. Send a text to the number.
  5. Read the form parameters and the signature header in the inbox above.

One honest caveat: the webhook URLs on this site answer with a small JSON body, not TwiML. Twilio still delivers the request — which is what you came to inspect — but it will record a content-type warning in its debugger for that message. Use this to look at requests, not as a production responder.

To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.

Things that catch people out

  • SHA-1 here is not a weakness to "fix". HMAC-SHA1 remains sound as a MAC; the known SHA-1 attacks are collision attacks that do not apply to HMAC. Use the algorithm Twilio uses or nothing will match.
  • Query strings count. If the configured URL has ?tenant=42, that is part of the signed string — in the same order and encoding Twilio used.
  • Status callbacks have a different parameter set. MessageStatus, ErrorCode and friends arrive on the status callback URL, and arrive out of order often enough that you should not assume sent precedes delivered.

api-tester.com is independent and is not affiliated with Twilio. Providers change header names, limits and retry schedules; treat the official Twilio documentation as the source of truth.

Related examples and tools

Other webhooks that sign the URL as well

References

These links leave api-tester.com, which is independent of the companies and projects they point to.

Twilio webhook FAQ

What does a Twilio webhook request look like?

An HTTP POST with a form-encoded body of parameters such as MessageSid, AccountSid, From, To, Body and NumMedia, plus an X-Twilio-Signature header. It is not JSON.

How do I validate X-Twilio-Signature?

Take the full URL Twilio requested, append every POST parameter name and value in alphabetical order by name, compute an HMAC-SHA1 with your Auth Token, base64-encode it and compare it to the header in constant time.

Why does Twilio signature validation fail behind a proxy?

Because the URL is part of the signed string. If a proxy terminates TLS, the application may rebuild the URL as http with an internal host, which does not match what Twilio signed. Use the public URL.

What should my endpoint return to a Twilio message webhook?

TwiML describing what to do, such as a Message reply, or an empty Response element or empty 2xx body to do nothing.

Does Twilio retry failed webhooks?

Not for HTTP error responses by default. Configure a fallback URL for the number, and use connection overrides if you need retries on connection failures.