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