How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a GitLab webhook?
A GitLab webhook is an HTTP POST sent to a URL configured on a project or group when a selected trigger fires — a push, a tag, a merge request change, a pipeline status, a comment. The body's object_kind names the event and the same information appears, in human form, in the X-Gitlab-Event header.
It behaves the same on GitLab.com and on a self-managed instance, with one practical difference: a self-managed administrator can change timeouts and limits, so the numbers you observe may not be the defaults.
What a GitLab webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Gitlab-Event: Push Hook
X-Gitlab-Token: your-secret-token
X-Gitlab-Event-UUID: 13792a34-cac6-4fda-95a8-c58e00a3954e
X-Gitlab-Webhook-UUID: 8d7d0f8b-1b0c-4b4e-9d2a-2f5d1c6e7a90
X-Gitlab-Instance: https://gitlab.com
Idempotency-Key: 9f3d2c1b-7a6e-4d5c-8b9a-0e1f2a3b4c5d
{
"object_kind": "push",
"event_name": "push",
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"ref": "refs/heads/main",
"ref_protected": true,
"checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"user_id": 4,
"user_name": "John Smith",
"user_username": "jsmith",
"project_id": 15,
"project": {
"id": 15,
"name": "Diaspora",
"web_url": "https://gitlab.example.com/mike/diaspora",
"path_with_namespace": "mike/diaspora",
"default_branch": "main"
},
"commits": [
{
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"message": "Fix retry backoff in webhook worker",
"timestamp": "2026-01-01T12:00:00+00:00",
"url": "https://gitlab.example.com/mike/diaspora/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"author": {
"name": "John Smith",
"email": "jsmith@example.com"
},
"added": [],
"modified": [
"worker/retry.rb"
],
"removed": []
}
],
"total_commits_count": 1
}
The commits array is capped at a small number of entries for large pushes; total_commits_count tells you how many there really were. Merge request, pipeline and note events use the same outer fields but put their detail in object_attributes.
Headers GitLab sends
| Header | Example | What it is |
|---|---|---|
X-Gitlab-Event | Push Hook | Human-readable event name: Push Hook, Tag Push Hook, Merge Request Hook, Pipeline Hook, Note Hook… |
X-Gitlab-Token | your-secret-token | The secret token you configured, sent as is. Present only when one is set. |
X-Gitlab-Event-UUID | 13792a34-cac6-4fda-95a8-c58e00a3954e | Id of the event. Shared by every webhook the same event triggers. |
X-Gitlab-Webhook-UUID | 8d7d0f8b-1b0c-4b4e-9d2a-2f5d1c6e7a90 | Id of this individual webhook request. |
X-Gitlab-Instance | https://gitlab.com | Base URL of the GitLab instance that sent it. |
Idempotency-Key | 9f3d2c1b-7a6e-4d5c-8b9a-0e1f2a3b4c5d | Stable across retries of the same webhook — the de-duplication key. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
The secret token is compared, not computed
GitLab's classic mechanism is the simplest of any major provider: whatever you type into the webhook's Secret token field is sent back to you verbatim in the X-Gitlab-Token header. There is no HMAC and nothing is derived from the body, so the header proves the sender knows the token but says nothing about whether the body was altered in transit. That makes HTTPS — with certificate verification left on — a requirement rather than a nicety.
Compare the header to your stored token in constant time. If the webhook form on your GitLab version offers a signing option in addition to the plain token, prefer it and follow the documentation for that version for the header and scheme — this page describes the token mechanism, which every version supports.
const crypto = require('crypto');
function verifyGitLab(req, secretToken) {
const a = Buffer.from(req.headers['x-gitlab-token'] || '');
const b = Buffer.from(secretToken);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Timeouts and auto-disabling
GitLab.com gives an endpoint ten seconds to respond, per its documentation at the time of writing. GitLab does not wait for your processing and does not care about the response body, so return a 2xx immediately.
A webhook that keeps failing is automatically disabled. Repeated 5xx responses or timeouts disable it temporarily with a growing back-off; repeated 4xx responses are treated as a configuration problem and can disable it until someone re-enables it by hand. A disabled hook is marked in the project's webhook list, and its Recent events page shows each request and response so you can resend one after fixing the receiver.
Catch a real GitLab webhook
- Press Create webhook URL at the top of this page and copy the address.
- In the project, open Settings → Webhooks and add a new webhook.
- Paste the address, enter a throwaway secret token to see the header, and tick the triggers you want.
- Save, then use the webhook's Test menu to send a sample push event.
- Read the headers and JSON in the inbox above.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Test events are built from real data. The Test button uses the project's latest commits, so an empty repository cannot send a test push.
- Returning
4xxfor "not interested" backfires. It counts toward disabling the hook. Return200for events you choose to ignore. - Local addresses are blocked by default. A self-managed instance refuses to call private network URLs unless an administrator allows it — an SSRF protection, not a bug.
api-tester.com is independent and is not affiliated with GitLab. Providers change header names, limits and retry schedules; treat the official GitLab documentation as the source of truth.
Related examples and tools
Telegram Webhook Example
The Update object Telegram posts to a bot's webhook, the optional secret-token header, and the trick of answering with an API call in the response body.
SendGrid Webhook Example
SendGrid's Event Webhook posts a batch — a JSON array of email events — and signs it with ECDSA rather than an HMAC.
WooCommerce Webhook Example
What a WooCommerce store posts when an order, product or customer changes — the X-WC-Webhook-* headers, the REST-shaped JSON, and the rule that switches a failing webhook off.
Mailchimp Webhook Example
Mailchimp audience webhooks are form posts with bracketed field names, and they carry no signature at all.
HubSpot Webhook Example
HubSpot delivers CRM events in batches: one request, a JSON array, each element a small notification that names a record without containing it.
GitHub Webhook Example
The same push event from GitHub, where the secret is used for an HMAC instead.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks that send a plain secret token
References
- Official GitLab documentation — The provider's own guide to the requests described on this page.
- GitLab Docs: webhook events — The payload reference for every event.
- GitLab Docs: project webhooks API — Creating, testing and resending webhooks over the API.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
GitLab webhook FAQ
What does a GitLab webhook payload look like?
A JSON object whose object_kind names the event. A push includes before, after, ref, user and project details, a commits array and total_commits_count; other events put their detail in object_attributes.
How do I verify a GitLab webhook?
Compare the X-Gitlab-Token header with the secret token you configured, using a constant-time comparison. The token is sent as is and is not an HMAC of the body, so always use HTTPS.
Which header contains the GitLab event type?
X-Gitlab-Event, with values such as Push Hook, Merge Request Hook or Pipeline Hook. The body's object_kind field carries the same information in machine form.
Why was my GitLab webhook disabled?
GitLab automatically disables webhooks that fail repeatedly. Server errors and timeouts disable it temporarily; repeated 4xx responses can disable it until it is re-enabled manually.
How long does GitLab wait for a webhook response?
Ten seconds on GitLab.com according to its documentation at the time of writing. Self-managed instances can be configured differently.