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.
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.