GitLab Webhooks: Setup, Verification and Failures

A GitLab webhook sends an HTTP POST to your URL when an event happens in a project or group, such as a push, a merge request or a new comment. Add one under Settings, Webhooks. Protect it with a signing token, and answer quickly with 200, because GitLab disables hooks that keep failing.

GitLab webhook facts (docs.gitlab.com, October 2026)
ItemWhat GitLab documents
Who can add a project hookMaintainer or Owner role
Group webhooksPremium and Ultimate tiers
Temporary disableAfter 4 consecutive failures, starting at 1 minute, growing up to 24 hours
Permanent disableAfter 40 consecutive failures
GitLab.com timeout10 seconds
GitLab.com hooks per project / group100 / 50
Recent events keptRequests from the last two days

Updated

Webhooks are how GitLab tells your site, deploy script or team chat that something changed, without your code asking over and over. Setup takes a minute; the parts that cause trouble later are proving a request came from GitLab and keeping the hook from being switched off. The checker below builds a signing-token signature in your browser so you can test your receiver.

Signing token signature builder

Enter a signing token, the webhook-id, the webhook-timestamp and the raw body to get the webhook-signature value GitLab would send. Everything runs in your browser.

The prefilled token is a made-up example, not a real key. With these values the result is: v1,3C1kB/w3aOegehfSwaRWhDLWlaxg12CAgTAGOwWAhQw=

When does GitLab disable a hook?

Consecutive failed deliveries: 0

Active: deliveries continue.

Verify a GitLab webhook in Node.js

This receiver checks the signing token signature, rejects stale timestamps, and falls back to the old secret token while you migrate.

gitlab-webhook.js

Tokens come from environment variables. The five-minute freshness window is our own choice; pick what suits you.

import crypto from "node:crypto";
import express from "express";

const app = express();
const SIGNING = process.env.GITLAB_SIGNING_TOKEN; // whsec_...
const SECRET = process.env.GITLAB_SECRET_TOKEN;   // optional, old style

function safeEqual(a, b) {
  const x = Buffer.from(a), y = Buffer.from(b);
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

app.post("/webhooks/gitlab", express.raw({ type: "application/json" }), (req, res) => {
  const sigHeader = req.get("webhook-signature");
  if (sigHeader && SIGNING) {
    const id = req.get("webhook-id");
    const ts = req.get("webhook-timestamp");
    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(400).send("Stale");
    const key = Buffer.from(SIGNING.replace(/^whsec_/, ""), "base64");
    const mac = crypto.createHmac("sha256", key).update(`${id}.${ts}.`).update(req.body).digest("base64");
    const ok = sigHeader.split(" ").some((s) => safeEqual(s, `v1,${mac}`));
    if (!ok) return res.status(401).send("Bad signature");
  } else if (!SECRET || !safeEqual(req.get("X-Gitlab-Token") || "", SECRET)) {
    return res.status(401).send("Bad token");
  }
  res.status(200).send("ok");
  console.log("Event:", req.get("X-Gitlab-Event"));
});

app.listen(3000);

Common events and their X-Gitlab-Event values

GitLab webhook events
EventX-Gitlab-Event header
PushPush Hook
Tag pushTag Push Hook
Issue or work itemIssue Hook
CommentNote Hook
Merge requestMerge Request Hook
PipelinePipeline Hook
JobJob Hook
DeploymentDeployment Hook
ReleaseRelease Hook
Wiki pageWiki Page Hook

How the signing token works

A signing token starts with whsec_. Drop that prefix and base64-decode the rest to get the key. GitLab signs the string made of the webhook-id, a dot, the webhook-timestamp, a dot and the raw body, using HMAC-SHA256.

The result is base64-encoded and sent in the webhook-signature header as v1, followed by the signature. GitLab follows the Standard Webhooks specification, so libraries written for that format can verify it too.

Moving from a secret token without downtime

A hook can carry both tokens at once. Set up the signing token, change your receiver to check the signature when it is present and fall back to X-Gitlab-Token when it is not, then remove the old secret.

How it works

  1. Open the project, then in the left sidebar choose Settings, Webhooks, and select Add new webhook.
  2. Enter the endpoint under URL, percent-encoding any special characters, and add a Name and Description if you like.
  3. Select Generate signing token and copy it straight away; GitLab displays it only once.
  4. Under Trigger, tick the events you want, such as push events or merge request events, then select Add webhook.
  5. Use the Test option to send a sample delivery, then open Edit and Recent events to see the request and your server's reply.

Tips

Frequently asked questions

What is X-Gitlab-Token?

It is the older secret token. When you set one, GitLab sends it as plain text in the X-Gitlab-Token header and your server compares it. GitLab's docs now recommend a signing token for new webhooks instead.

Why was my GitLab webhook disabled?

GitLab disables a hook after four consecutive failures, meaning 4xx or 5xx responses, connection timeouts or other HTTP errors. It is first off for one minute, longer with each further failure up to 24 hours, and permanently after 40 in a row.

How do I tell which event arrived?

Read the X-Gitlab-Event header. Its value is the event type followed by Hook, for example Push Hook, Merge Request Hook, Note Hook for comments or Pipeline Hook.

Can I add a webhook to a whole group?

Yes, on the Premium and Ultimate tiers. A group webhook fires for events in every project of the group, and can also receive group member, project and subgroup events.

Does the signing token work on self-managed GitLab?

GitLab's docs say it arrived in version 19.0 behind a feature flag that was on by default and became generally available in 19.1. Older self-managed versions only have the secret token.

Related: GitHub webhooks, Webhook vs API, Stripe webhooks, Discord webhooks.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap