Receive Twilio webhooks reliably
This guide puts Railhook between Twilio and your application for Twilio’s informational webhooks, such as message status callbacks. Twilio sends them to a Railhook source, which verifies the signature, stores each one as it arrived and forwards it to your service with retries.
Why Twilio webhooks need a buffer
Section titled “Why Twilio webhooks need a buffer”By default Twilio waits 5 seconds to connect and 15 seconds for a response, and retries a webhook once, only when the connection itself failed. A 5xx from your app or a slow response is not retried unless you add connection overrides to the URL. (Twilio: Webhooks connection overrides)
So a status callback that arrives while your app is restarting is usually lost, and your records of which messages were delivered drift from Twilio’s. With Railhook in front:
- Twilio is answered as soon as the callback is stored.
- Your service gets up to 5 attempts per forward, with waits from 1 minute to 1 hour, and what still fails lands in Failed Forwards.
- A callback carrying an
I-Twilio-Idempotency-TokenRailhook has already stored is answered with the stored copy and not forwarded again.
Connect Twilio
Section titled “Connect Twilio”You need a Railhook project and an API key. Everything below can also be done under Incoming in the dashboard.
export RAILHOOK_URL=https://railhook.io # or your own instanceexport RAILHOOK_API_KEY=...export PROJECT_ID=...-
Create the source with your Twilio auth token, from the Twilio Console. Twilio signs its requests with it:
Terminal window curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources" \-H "X-API-Key: $RAILHOOK_API_KEY" \-H "Content-Type: application/json" \-d '{"name":"Twilio","providerType":"TWILIO","verificationMode":"PROVIDER","hmacSecret":"'"$TWILIO_AUTH_TOKEN"'"}'Keep the
idand theingressUrlfrom the response. -
Add a destination, the URL of your own service:
Terminal window curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-sources/$SOURCE_ID/destinations" \-H "X-API-Key: $RAILHOOK_API_KEY" \-H "Content-Type: application/json" \-d '{"url":"https://api.example.com/webhooks/twilio","authType":"BEARER","authConfig":"{\"token\":\"...\"}","enabled":true}' -
Give Twilio the URL. Pass the
ingressUrlexactly as the source shows it wherever Twilio takes a status callback URL, such as theStatusCallbackparameter when you send a message. Keep the methodPOST: the ingress accepts nothing else. -
Send a message. Its status callbacks appear under Incoming → Received, with one forward per destination.
How the signature is checked
Section titled “How the signature is checked”Twilio’s X-Twilio-Signature is a base64 HMAC-SHA1 computed with the auth token. For a form-encoded request it covers the URL followed by every parameter sorted by name; for other requests it covers the URL, and the bodySHA256 query parameter must match the body. A request that does not match is answered 401 and not stored.
What your service receives
Section titled “What your service receives”The body exactly as Twilio sent it, byte for byte, with its form-encoded Content-Type, so your service parses it as it would a direct callback. X-Twilio-Signature is not forwarded, and it would not verify against your URL anyway: your service authenticates Railhook through the destination’s credentials instead. See Destinations.
Develop locally with the CLI
Section titled “Develop locally with the CLI”-
Install the CLI and log in, as in CLI, then open a tunnel to your app’s port:
Terminal window railhook listen 3000 -
Add a second destination to the source with the printed public URL and your route appended, such as
https://<host>/tunnel/<slug>/webhooks/twilio. It reacheshttp://localhost:3000/webhooks/twilio. -
Send a test message with the ingress URL as its status callback, and each callback arrives in your local app. Disable that destination when you stop the tunnel.
Replay after an outage
Section titled “Replay after an outage”Replay what arrived while your service was down. Each replayed callback goes to every destination of the source as a new forward:
curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/incoming-events/bulk-replay" \ -H "X-API-Key: $RAILHOOK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"sourceId":"'"$SOURCE_ID"'","from":"2026-09-18T10:00:00Z","to":"2026-09-18T14:00:00Z","verified":true}'Replay reaches back as far as events are kept: 7 days on Railhook Cloud, and whatever you configure when self-hosting. A status callback describes a state, so applying the same one twice should be harmless; make sure your handler treats it that way. See Replay.