Receive GitLab webhooks reliably
This guide puts Railhook between GitLab and your application. GitLab sends its webhooks to a Railhook source, which checks the secret token, stores each webhook as it arrived and forwards it to your service with retries. GitLab only ever talks to Railhook, which answers straight away, so your service being slow or down no longer counts against the webhook.
Why GitLab webhooks need a buffer
Section titled “Why GitLab webhooks need a buffer”GitLab disables a webhook that keeps failing. After four consecutive failures it is disabled temporarily, for one minute at first and for up to 24 hours as failures continue; after 40 consecutive failures it is disabled permanently and is not re-enabled on its own. GitLab also asks you to respond before its timeout and to be ready for duplicate events when a webhook times out. (GitLab: Webhooks)
A few bad deploys can therefore switch your integration off without anyone noticing. With Railhook in front:
- GitLab’s requests are answered as soon as they are stored, so they do not time out or fail because of your service.
- 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 duplicate carrying the same
webhook-idorIdempotency-Keyheader is answered with the stored copy and not forwarded again.
Connect GitLab
Section titled “Connect GitLab”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 token. With GitLab you pick it yourself:
Terminal window export GITLAB_WEBHOOK_TOKEN=$(openssl rand -hex 32) -
Create the source with that token as the 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":"GitLab","providerType":"GITLAB","verificationMode":"PROVIDER","hmacSecret":"'"$GITLAB_WEBHOOK_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/gitlab","authType":"BEARER","authConfig":"{\"token\":\"...\"}","enabled":true}' -
Register the webhook in GitLab. In the project or group, open Settings → Webhooks and add a new webhook. Paste the
ingressUrlas the URL, the same token as the Secret token, and select the triggers you want. -
Send a test with the webhook’s Test button. It appears under Incoming → Received, with one forward per destination.
How the token is checked
Section titled “How the token is checked”GitLab does not sign the body; it sends the secret token in the X-Gitlab-Token header. Railhook checks that the header equals the source’s secret and answers 401, storing nothing, when it does not. Because a token is not a signature, keep the ingress URL private and serve it over HTTPS.
What your service receives
Section titled “What your service receives”The body exactly as GitLab sent it, byte for byte. Railhook passes GitLab’s event headers through, X-Gitlab-Event, X-Gitlab-Event-UUID and X-Gitlab-Instance, so your service can tell a push from a merge request. X-Gitlab-Token is never forwarded: 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/gitlab. It reacheshttp://localhost:3000/webhooks/gitlab. -
Click Test on the GitLab webhook, and the webhook arrives in your local app with its
X-Gitlab-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 webhook 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. Make your handler idempotent on what the webhook is about, such as the commit or merge request in the body, since a replay can send webhooks your service already processed. See Replay.