Skip to content
newThe webhook.co MCP server is live — turn any webhook into an agent event. Read the docs
test locally

Test Twilio webhooks locally

Twilio has two webhook systems that behave differently enough to count as two integrations. Here's what changes depending on which one is pointed at your machine.

The loop

Create an endpoint and it prints a permanent ingest URL. Everything sent there is captured whole — headers, body, the exact bytes — before anything else happens. Stream those events to a port on your machine, and replay any one of them as often as you like while you fix the handler. Forwarding runs from your own machine, so it isn't a billable delivery.

wbhktwilio
$ wbhk endpoints create twilio-dev
https://wbhk.my/whep_…
$ wbhk listen <endpoint-id> --forward http://localhost:3000/webhooks
$ wbhk replay <event-id> --forward http://localhost:3000/webhooks

What's different about Twilio

The thing that catches people out is that Twilio doesn't have a webhook system — it has two, and they share almost nothing. Classic webhooks hang off a phone number, have no event names at all, and expect TwiML back. Event Streams is account-wide: you create a webhook Sink, then a Subscription naming event types and a schema version, and Twilio POSTs JSON. Timeouts differ, failure semantics differ, and a test facility exists for one and not the other. Work out which one you're receiving before you debug anything, because the answers are not interchangeable.

Where the URL goes

For a phone number, set the webhook URL in the Twilio Console, or POST to the IncomingPhoneNumbers resource with SmsUrl, SmsMethod, VoiceUrl, VoiceMethod or StatusCallback — the Twilio CLI updates the same fields. Event Streams needs two objects instead: a Sink with sink_type webhook, whose sink_configuration carries your destination and method, then a Subscription listing each event type alongside its schema_version.

Event Streams has a real one: a Sink Test endpoint at events.twilio.com that initiates a test event on a Sink and answers with a result of submitted. What lands on your endpoint isn't documented, so watch what actually arrives rather than assuming. Phone-number webhooks have no documented test-send control. The route Twilio documents is live traffic — message or call the number — then the Console Debugger, which lists any errors Twilio finds when it sends a webhook to your application.

What Twilio sends

Only Event Streams names things. In practice the identifiers are reverse-DNS and dot-separated: product, then resource, then a past-tense action. Each type is pinned to a numbered schema_version that advances per event type rather than globally, and Twilio recommends subscribing to the latest version of an event.

  • com.twilio.messaging.inbound-message.received
  • com.twilio.messaging.message.queued
  • com.twilio.messaging.message.sent
  • com.twilio.messaging.message.delivered
  • com.twilio.messaging.message.undelivered
  • com.twilio.messaging.message.failed

Worth knowing first

Two timeout budgets, not one

A webhook Sink response gets a maximum of five seconds. A classic webhook gets a 5000 ms connect timeout, a 15000 ms read timeout and 15000 ms total time, with a retry count of 1 and a retry policy of ct by default. Those overrides ride along as fragments on the webhook URL, and aren't available for Twilio Conversations or Frontline. Voice adds a hard 15-second cap on all call-related HTTP requests.

Event Streams is at-least-once

Twilio retries a failed sink delivery for up to four hours, with exponential backoff plus jitter, so there's no fixed retry count to plan around. Retries mean duplicates. The prescribed fix is to key on the event id: discard if you've seen it, store it if you haven't, and return 200 even for the discards, or Twilio keeps retrying.

Status callbacks arrive out of order

There's no guarantee that status callback requests reach your endpoint in the order they were sent, so a queued landing after a delivered is normal rather than a bug. Twilio wants an HTTP 200 back and requires no response content. Write the state machine to tolerate transitions in any order instead of assuming a sequence.

You can't allowlist Twilio's IPs

Twilio uses a cloud architecture and doesn't have a fixed range of IP addresses that issue webhooks. Authenticate on the X-Twilio-Signature header instead: HMAC-SHA1 over the exact URL you gave Twilio plus the request parameters, keyed with your account auth token. On WebSocket requests the header name is lowercase. Twilio also won't connect to an HTTPS URL with a self-signed certificate.

Fallback URLs are a separate mechanism

SmsFallbackUrl and VoiceFallbackUrl fire when an error occurs requesting or executing the TwiML from the primary URL. That's a different trigger from the connection-override retry policy — one covers the TwiML your application returns, the other covers the connection used to fetch it. Conflating them makes fallback traffic look like a network problem.

The sink body is an array

An Event Streams webhook delivers a JSON array. Today it contains only one event, and Twilio's own advice is to iterate anyway to future-proof the implementation. A handler that reads element zero and moves on works fine right up until the day the array grows a second entry.

Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.

Twilio webhooks: common questions

Does Twilio let you send a test webhook?

For Event Streams, yes — there's an API endpoint that initiates a test event on a Sink and responds with a result of submitted. For phone-number webhooks, Twilio documents no test-send control. The documented approach there is real traffic plus the Console Debugger.

How do I verify a webhook actually came from Twilio?

Check the X-Twilio-Signature header. It's an HMAC-SHA1 over the exact URL you gave Twilio plus the request parameters, keyed with your account auth token. IP allowlisting isn't an option, because Twilio has no fixed range of addresses that issue webhooks. On WebSocket requests the header name is lowercase.

Can Twilio Event Streams deliver the same event twice?

Yes. Event Streams guarantees at-least-once delivery, and retries can produce duplicates. Twilio's guidance is to check whether you already received an event with the same id, discard the incoming event if so, store the id if not, and return 200 even for the discards.

How long does Twilio wait for my endpoint to respond?

It depends which system you're on. A webhook Sink response has a maximum timeout of five seconds. A classic phone-number webhook defaults to a 5000 ms connect timeout with 15000 ms read and total time, tunable per URL, and call-related requests carry a hard upper timeout of 15 seconds.

Does Twilio require a publicly reachable URL?

For phone-number webhooks, yes — Twilio needs to send webhook requests to a publicly available URL, so a bare localhost address won't do. It also won't connect to an HTTPS URL with a self-signed certificate, which rules out most quick local TLS setups.

Point a webhook at it. Watch it land.

Start free: a permanent URL, full inspection, one-command replay — and outbound delivery, because every feature is on every plan. Move up when you need more events.