Skip to content
TryWebhook
New URL

Messaging · Free · No signup

Test and debug Twilio webhooks

Point a Twilio status callback or messaging webhook at a throwaway URL and read what actually arrives: form-encoded fields, not JSON, and a signature computed over the URL as well as the body.

No signup · No email · Ready in about a second

At a glance

Twilio webhooks in brief

Category
Messaging
Where to configure
Phone Numbers → Manage → Active numbers → your number → Messaging or Voice webhook URL
Signature header
X-Twilio-Signature
Secret
Auth token (from the Console dashboard)
Retries
None for status callbacks — Twilio fires once and moves on. For TwiML requests, a failure falls through to the number's Fallback URL if one is set.
Response deadline
15 seconds, with about 10 of those available before Twilio gives up connecting.
Treated as success
Any 2xx for status callbacks. TwiML endpoints must also return valid XML or the call or message fails.

Cross-checked against Twilio's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Twilio actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}237 B

Form-encoded, not JSON — the case that breaks most handlers.

1 headers
user-agent
TwilioProxy/1.1
Body · application/x-www-form-urlencoded · 1 line
SmsSid=SM00000000000000000000000000000000&SmsStatus=delivered&MessageStatus=delivered&To=%2B15558675310&MessageSid=SM00000000000000000000000000000000&AccountSid=AC00000000000000000000000000000000&From=%2B15551234567&ApiVersion=2010-04-01

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Twilio signature

Signed payload

A base64 HMAC-SHA1 over the full request URL — scheme, host, path, and query — followed by every POST parameter name and value concatenated in alphabetical order by name. Keyed with your account auth token.

Implemented as: HMAC-SHA1 over the full URL plus, for form posts, every parameter sorted by name and concatenated as name+value. Base64.

Built in. Open any captured request, switch to the Signature tab, pick Twilio, and paste your auth token (from the console dashboard). The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
MessageStatus=deliveredThe carrier confirmed handset delivery. Not every carrier reports it, so `sent` is sometimes the last status you will see.
MessageStatus=failedDelivery failed permanently. Read `ErrorCode` — 30003 is an unreachable handset, 30007 is carrier filtering, and they need different responses.
MessageStatus=undeliveredThe carrier accepted then rejected the message. Usually filtering or a blocked number.
CallStatus=completedA call ended. `CallDuration` and `RecordingUrl` arrive on this callback, not earlier ones.
Incoming SMSSomeone texted your number. Twilio expects TwiML back, and an empty `<Response/>` is how you accept without replying.

Pointing a Twilio callback at a test URL

Create a URL above, then set it as a callback. For SMS status, the quickest route is the API call itself:

curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$ACCOUNT_SID/Messages.json" \
  --data-urlencode "To=+15558675310" \
  --data-urlencode "From=$TWILIO_NUMBER" \
  --data-urlencode "Body=hello" \
  --data-urlencode "StatusCallback=https://trywebhook.com/w/your-bin-id" \
  -u "$ACCOUNT_SID:$AUTH_TOKEN"

You will get several deliveries in sequence — queued, then sent, then delivered — each a separate POST. Watch them arrive in order in the list.

For incoming messages, set the URL under Phone Numbers → Manage → Active numbers → your number → A message comes in. Twilio will then expect TwiML in the response, which a capture bin does not return; the message still arrives here, and Twilio logs an error about the response. That is expected and harmless while you are inspecting.

Form-encoded, and what that means

The body of the sample above is one line:

SmsSid=SM00000000000000000000000000000000&SmsStatus=delivered&MessageStatus=delivered&To=%2B15558675310&...

No JSON anywhere. Phone numbers are percent-encoded, so +15558675310 arrives as %2B15558675310 and a handler that treats it as a literal string will store a space where the plus should be.

Reading it correctly:

const form = await request.formData();
const status = form.get('MessageStatus');
const sid = form.get('MessageSid');

MessageSid is your join key back to the send. SmsSid is the same value under an older name, and both are present for compatibility.

Error codes carry the real information

On a failed or undelivered status you also get ErrorCode. It is the difference between a problem you can fix and one you cannot:

  • 30003 — unreachable destination handset. Retry later, maybe.
  • 30005 — unknown destination. The number is wrong; stop trying.
  • 30007 — carrier filtering. Your content or sender reputation triggered a block.
  • 30008 — unknown error from the carrier. Genuinely opaque.

Verifying X-Twilio-Signature

This is the most unusual scheme of any provider on this site, because the signature covers more than the body. Twilio builds the signed string by taking the full URL it called, then appending each POST parameter’s name and value, sorted alphabetically by name, with no separators:

https://trywebhook.com/w/your-bin-idAccountSidAC00...ApiVersion2010-04-01From+15551234567MessageSid...

Then HMAC-SHA1 with your auth token, base64-encoded. Two consequences worth internalising:

The URL must match exactly. Scheme, host, port, path, and query string. Behind a load balancer that terminates TLS, your framework will reconstruct http:// and the digest will differ. Every official Twilio helper library takes the URL as an explicit argument precisely so you can pass the public one.

Parameter order is defined by sorting, not by arrival. So the raw body order does not matter for verification — but the values must be the decoded ones, not the percent-encoded ones.

In the Signature tab above, choose Twilio and paste your auth token. The URL is taken from the request as captured, including the path and query, which is usually the piece that reveals the mismatch.

Not retried, so log first

Status callbacks fire once. There is no delivery log in the Console you can replay from, and no retry to save you. Whatever your handler does, it should record the raw form body before it does anything that can throw — a parse error on one unexpected field should not lose the delivery notification entirely.

Failure modes

Where Twilio integrations usually break

It is form-encoded, not JSON

`application/x-www-form-urlencoded`, always. A handler wired up with a JSON body parser sees an empty object and no error, which is the single most reported Twilio integration problem. Read the body as form data.

The signature covers the URL

Twilio signs the exact URL it called, including scheme, port, and query string. If your app sits behind a proxy that terminates TLS, the URL your framework reconstructs will say `http` and the signature will never match. Build the URL from `X-Forwarded-Proto` and the configured host, not from the request as your framework sees it.

A query string in the URL changes the signature

For POST requests, query parameters are part of the signed URL while form fields are appended separately. Adding `?env=staging` to a callback URL is enough to break verification if your validator drops the query.

Status callbacks are not retried

If your endpoint is down when a `delivered` status fires, that status is gone. There is no delivery log to replay from. Reconcile through the Messages API if you need certainty.

SendGrid is not this

SendGrid is a Twilio product but its Event Webhook is signed with ECDSA and a public key, not an HMAC and your auth token. The header even looks similar — `X-Twilio-Email-Event-Webhook-Signature`. Different provider, different verification.

Answers

Twilio webhook questions

Why is my Twilio webhook body empty?+

Because it is form-encoded and your framework is parsing it as JSON. In Express that means `express.urlencoded()` rather than `express.json()`; with the Fetch API it means `await request.formData()`. The capture above shows the raw bytes, which makes the content type obvious.

Can I verify X-Twilio-Signature here?+

Yes. Open the request, choose Twilio in the Signature tab, and paste your auth token. The URL that Twilio signed is reconstructed from the request as captured, which is exactly the piece your own server usually gets wrong.

What is the difference between a status callback and a TwiML webhook?+

A TwiML webhook is a question — Twilio asks what to do with an incoming call or message and expects XML back. A status callback is a notification — it tells you what happened and ignores your response body. Only the second one is a webhook in the usual sense.

How do I test without sending real SMS?+

Send from a Twilio trial number to your own verified number; it costs cents and produces real callbacks with real signatures. The Send test webhook panel here fires the same form-encoded shape with no account at all, which is enough to check your parsing.

Why does verification pass in production but fail locally?+

Because the signed URL differs. Locally your app sees `http://localhost:3000/webhook`; Twilio signed `https://your-domain/webhook`. Validators take the URL as an argument for exactly this reason — pass the public one.

Other providers

Testing something else?

One click

Point Twilio at a URL and watch the payload land.

Phone Numbers → Manage → Active numbers → your number → Messaging or Voice webhook URL — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second