Skip to content
newThe webhook.co MCP server is live — turn any webhook into an agent event. Read the docs
test locally

Test GitLab webhooks locally

GitLab webhooks live on a project or a group, name their events three different ways at once, and switch themselves off if your endpoint misbehaves four times running.

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.

wbhkgitlab
$ wbhk endpoints create gitlab-dev
https://wbhk.my/whep_…
$ wbhk listen <endpoint-id> --forward http://localhost:3000/webhooks
$ wbhk replay <event-id> --forward http://localhost:3000/webhooks

What's different about GitLab

GitLab names the same event three different ways, and you'll meet all three. The X-Gitlab-Event header carries a title-cased string like Merge Request Hook. The JSON body carries the same event in snake case as object_kind, reading merge_request. The API subscribes with boolean flags that are plural where the header is singular: merge_requests_events. Route on whichever you prefer, but expect the other two in logs and bug reports. A second trap sits underneath: an Issue Hook doesn't always carry object_kind issue — for other work items the field reads work_item.

Where the URL goes

You need the Maintainer or Owner role on a project, or Owner on a group. From the top bar use Search or go to, find the project or group, then Settings > Webhooks > Add new webhook. Enter the URL, percent-encoding any special characters. GitLab ranks the request-authentication options for you: signing token recommended, secret token explicitly not recommended. Choose your events in the Trigger section and select Add webhook.

Each saved webhook gets a Test dropdown: pick an event type and GitLab posts a sample payload. Not every event type is testable, and a push test needs the project to have at least one commit. Past deliveries sit under Recent events with a Resend Request button that replays the same body under the same Idempotency-Key — though not once you've changed the webhook URL. Both test and resend are capped at five requests per minute.

What GitLab sends

These are X-Gitlab-Event header values, GitLab's user-facing name for each hook: title case, singular, each suffixed with Hook. The body's object_kind gives you the same event in snake case, and the API's subscription flags give a third, pluralised form of it.

  • Push Hook
  • Tag Push Hook
  • Issue Hook
  • Merge Request Hook
  • Note Hook
  • Pipeline Hook

Worth knowing first

Four failures and it switches off

GitLab.com disables a webhook that fails four consecutive times — 4xx, 5xx, timeouts and other HTTP errors all count. The first block lasts a minute and backs off toward 24 hours before automatically re-enabling; 40 consecutive failures disable it permanently. A test request returning 2xx brings it back. On GitLab Self-Managed, auto-disabling of project webhooks sits behind the auto_disabling_web_hooks feature flag, and the permanent 40-failure state arrived in 17.11.

Ten seconds is the whole budget

On GitLab.com the delivery timeout is 10 seconds, and a timeout counts toward auto-disabling. The docs are blunt about the shape they want: answer 200 or 201 quickly, put the work on a queue, don't process it inside the request. Self-managed instances set their own value through the gitlab_rails webhook_timeout setting, which the configuration example in the docs shows at 60 seconds.

Local addresses are blocked on self-managed

GitLab Self-Managed and Dedicated refuse webhook requests to the instance's own address and to private ranges — 127.0.0.1, ::1, 0.0.0.0, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local — until an administrator selects Allow requests to the local network from webhooks and integrations under Admin > Settings > Network > Outbound requests. On GitLab.com that setting isn't yours to change. Either way, give GitLab a public URL.

A large push can send nothing

GitLab caps how many branches or tags one push may touch before the push event stops firing at all: push_event_hooks_limit, which kicks in above three. A branch-heavy push therefore produces silence rather than a burst, which looks exactly like a dead endpoint when the endpoint is fine. Worth ruling out before you go hunting through your own logs.

One noisy webhook stalls the namespace

Webhook calls are rate-limited per top-level namespace, and every project and group webhook in that namespace shares the budget — 500 per minute on Free, rising with plan and seat count to 13,000. Hit the ceiling and GitLab temporarily disables all webhooks in the namespace until the next minute, not only the one that overran.

Duplicates are yours to absorb

GitLab publishes no delivery guarantee, but does tell you to prepare for duplicate events when a webhook times out. Every request carries an Idempotency-Key header, with webhook-id as the newer name for the same value, and the value survives retries and manual resends — so dedupe on it. Recent events keeps only the last two days, so debug while the evidence is still there.

Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.

GitLab webhooks: common questions

Does GitLab have a way to send a test webhook?

Yes. Each saved webhook has a Test dropdown that fires a sample payload for the event type you pick. Testing is not supported for every event type, and a push test needs the project to have at least one commit. Test and resend are both limited to five requests per minute.

Why did my GitLab webhook stop firing?

Most likely GitLab disabled it. On GitLab.com, four consecutive failures, counting 4xx, 5xx, timeouts and other HTTP errors, temporarily disable a webhook, backing off from one minute toward 24 hours; forty consecutive failures disable it for good. A test request returning 2xx re-enables it. On GitLab Self-Managed, auto-disabling of project webhooks sits behind the auto_disabling_web_hooks feature flag.

Can I point a GitLab webhook at localhost?

Not directly. GitLab Self-Managed and Dedicated block requests to private network ranges until an administrator enables outbound requests to the local network, and on GitLab.com that setting is not available to you at all. Give GitLab a publicly reachable URL and forward from there.

How does GitLab tell me which event fired?

Three ways at once. The X-Gitlab-Event header carries a title-cased name such as Merge Request Hook. The JSON body repeats the same event in lower-case snake form. The API subscribes using plural boolean flags. Route on whichever you like, but expect all three in the wild.

Does GitLab retry a failed webhook delivery?

Not in any documented way. GitLab describes automatic disabling of failing webhooks and a manual Resend Request button, but publishes no retry count or backoff schedule. It does tell you to expect duplicate events after a timeout, so dedupe on the idempotency key.

Which IP addresses does GitLab.com send webhooks from?

GitLab.com sends webhook traffic from its Web and API fleet range, 34.74.90.64/28 and 34.74.226.0/24, allocated solely to GitLab. If your receiver sits behind an allowlist, those are the ranges to permit. The no static IPs caveat in the docs applies to CI/CD runners, not to webhooks.

Point a webhook at it. Watch it land.

Start free: a permanent URL, full inspection, one-command replay — and outbound delivery, because every feature is on every plan. Move up when you need more events.