Skip to content

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.

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-Token Railhook has already stored is answered with the stored copy and not forwarded again.

You need a Railhook project and an API key. Everything below can also be done under Incoming in the dashboard.

Terminal window
export RAILHOOK_URL=https://railhook.io # or your own instance
export RAILHOOK_API_KEY=...
export PROJECT_ID=...
  1. 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 id and the ingressUrl from the response.

  2. 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}'
  3. Give Twilio the URL. Pass the ingressUrl exactly as the source shows it wherever Twilio takes a status callback URL, such as the StatusCallback parameter when you send a message. Keep the method POST: the ingress accepts nothing else.

  4. Send a message. Its status callbacks appear under Incoming → Received, with one forward per destination.

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.

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.

  1. Install the CLI and log in, as in CLI, then open a tunnel to your app’s port:

    Terminal window
    railhook listen 3000
  2. 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 reaches http://localhost:3000/webhooks/twilio.

  3. 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 what arrived while your service was down. Each replayed callback goes to every destination of the source as a new forward:

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