Receive GitHub webhooks reliably
This guide puts Railhook between GitHub and your application. GitHub sends its webhooks to a Railhook source, which verifies the signature, stores each delivery as it arrived and forwards it to your service with retries. When your service is down, the webhooks wait in Railhook instead of being lost.
Why GitHub webhooks need a buffer
Section titled “Why GitHub webhooks need a buffer”GitHub states that it does not automatically redeliver failed webhook deliveries. You can redeliver them by hand or through the API, but only deliveries from the past 3 days. (GitHub: Handling failed webhook deliveries, Redelivering webhooks) GitHub also asks your server to answer with a 2xx within 10 seconds. (Best practices for using webhooks)
So one deploy, one crash or one slow handler at the wrong moment, and that push, pull request or issue event is gone unless someone notices. With Railhook in front:
- Railhook answers GitHub as soon as the delivery is stored, well inside the 10 seconds.
- Your service gets up to 5 attempts per forward, with waits from 1 minute to 1 hour.
- What still fails lands in Failed Forwards, and any stored delivery can be replayed for as long as events are kept.
- A GitHub redelivery carries the same
X-GitHub-Deliveryid. Railhook recognises it, answers202with the stored copy and does not forward it twice.
Connect GitHub
Section titled “Connect GitHub”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=...-
Choose a secret. With GitHub you pick the webhook secret yourself:
Terminal window export GITHUB_WEBHOOK_SECRET=$(openssl rand -hex 32) -
Create the source with that secret:
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":"GitHub","providerType":"GITHUB","verificationMode":"PROVIDER","hmacSecret":"'"$GITHUB_WEBHOOK_SECRET"'"}'Keep the
idand theingressUrlfrom the response. -
Register the webhook in GitHub. In the repository or organization, open Settings → Webhooks → Add webhook. Paste the
ingressUrlas the Payload URL, set Content type toapplication/json, paste the same secret into Secret, and choose the events you want. -
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/github","authType":"BEARER","authConfig":"{\"token\":\"...\"}","enabled":true}' -
Check the ping. GitHub sends a
pingevent when the webhook is created. It appears under Incoming → Received in Railhook.
How the signature is checked
Section titled “How the signature is checked”Railhook reads X-Hub-Signature-256, which is sha256= followed by the hex HMAC-SHA256 of the raw body, and compares it with its own computation using the source’s secret. A request that does not match is answered 401 and not stored. A signature already seen within the replay window, 5 minutes by default, is refused too, unless the request is a redelivery of a delivery Railhook already stored.
What your service receives
Section titled “What your service receives”The body exactly as GitHub sent it, byte for byte. Because a push and an issue cannot be told apart by their bodies, Railhook passes GitHub’s event headers through: X-GitHub-Event, X-GitHub-Delivery, X-GitHub-Hook-ID, X-GitHub-Hook-Installation-Target-Type and X-GitHub-Hook-Installation-Target-ID. X-Hub-Signature-256 is not forwarded: your service authenticates Railhook through the destination’s credentials, and does not need the GitHub secret. 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/github. It reacheshttp://localhost:3000/webhooks/github. -
Push a commit, or replay the stored
pingfrom Railhook (a redelivery from GitHub carries the sameX-GitHub-Delivery, so Railhook answers it with the stored copy and does not forward it again). The verified webhook arrives in your local app with itsX-GitHub-Eventheader. 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 delivery 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}'Unlike GitHub’s own redelivery, this is not limited to 3 days: it reaches back as far as events are kept, 7 days on Railhook Cloud and whatever you configure when self-hosting. Make your handler idempotent on X-GitHub-Delivery, since a replay can send deliveries your service already processed. See Replay.