Agent skill
GitLab Webhooks Skill
Receive and verify GitLab webhooks. Use when setting up GitLab webhook handlers, debugging signature or token verification, or handling repository events like push, merge_request, issue, pipeline, or release.
Install this skill
npx skills add hookdeck/webhook-skills --skill gitlab-webhooks
When to Use This Skill
- Setting up GitLab webhook handlers
- Debugging webhook signature (signing token) or secret token verification failures
- Understanding GitLab event types and payloads
- Handling push, merge request, issue, or pipeline events
Essential Code (USE THIS)
GitLab has two ways to authenticate a webhook, and both can be set on the same webhook:
- Signing token (recommended, GitLab 19.0+, GA in 19.1): GitLab follows the Standard Webhooks spec. It signs
{webhook-id}.{webhook-timestamp}.{raw body}with HMAC-SHA256, using the signing token withwhsec_stripped and base64-decoded as the key, and sendswebhook-signature: v1,<base64>(a space-separated list; GitLab currently sends one). - Secret token (legacy): a plain-text value sent back in
X-Gitlab-Token. GitLab says it is "not recommended for new webhooks". Self-managed instances before 19.0 only have this option.
GitLab Signature Verification (JavaScript)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (use express.raw, not express.json)
function verifyGitLabSignature(rawBody, headers, signingToken) {
const id = headers['webhook-id'];
const ts = headers['webhook-timestamp'];
const sigHeader = headers['webhook-signature'];
if (!signingToken || !id || !ts || !sigHeader) return false;
// GitLab: check the timestamp is "recent" (5 min = Standard Webhooks library default)
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false;
const key = Buffer.from(signingToken.replace(/^whsec_/, ''), 'base64');
const digest = crypto.createHmac('sha256', key)
.update(`${id}.${ts}.`).update(rawBody).digest('base64');
const expected = Buffer.from(`v1,${digest}`);
return sigHeader.split(' ').some((sig) => {
const received = Buffer.from(sig);
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
Legacy Secret Token (X-Gitlab-Token)
function verifyGitLabToken(tokenHeader, secret) {
if (!tokenHeader || !secret) return false;
const a = Buffer.from(tokenHeader);
const b = Buffer.from(secret);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
While migrating, GitLab suggests verifying the signature when webhook-signature is present and falling back to the secret token otherwise. The examples do exactly that; a request that carries a signature never falls back to the token.
Python Signature Verification (FastAPI)
import base64, hashlib, hmac, time
def verify_gitlab_signature(raw_body: bytes, headers, signing_token: str) -> bool:
msg_id, ts = headers.get("webhook-id"), headers.get("webhook-timestamp")
sig_header = headers.get("webhook-signature")
if not (signing_token and msg_id and ts and sig_header):
return False
if abs(int(time.time()) - int(ts)) > 300:
return False
key = base64.b64decode(signing_token.removeprefix("whsec_"))
digest = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
expected = "v1," + base64.b64encode(digest).decode()
return any(hmac.compare_digest(expected, s) for s in sig_header.split(" "))
For complete working examples with tests, see:
- examples/express/ - Full Express implementation
- examples/nextjs/ - Next.js App Router implementation
- examples/fastapi/ - Python FastAPI implementation
Common Event Types
| Event | X-Gitlab-Event Header | object_kind | Description |
|---|---|---|---|
| Push | Push Hook | push | Commits pushed to branch |
| Tag Push | Tag Push Hook | tag_push | New tag created |
| Issue | Issue Hook | issue | Issue opened, closed, updated |
| Comment | Note Hook | note | Comment on commit, MR, issue |
| Merge Request | Merge Request Hook | merge_request | MR opened, merged, closed |
| Wiki | Wiki Page Hook | wiki_page | Wiki page created/updated |
| Pipeline | Pipeline Hook | pipeline | CI/CD pipeline status |
| Job | Job Hook | build | CI job status |
| Deployment | Deployment Hook | deployment | Environment deployment |
| Release | Release Hook | release | Release created |
For full event reference, see GitLab Webhook Events
Important Headers
| Header | Description |
|---|---|
webhook-signature | v1,<base64> HMAC-SHA256 signature(s), space-separated. Sent only when a signing token is configured |
webhook-id | Unique message ID, the same across retries. Part of the signed content |
webhook-timestamp | Unix timestamp (seconds) of the request. Part of the signed content |
X-Gitlab-Token | Legacy secret token, sent as plain text. Sent only when a secret token is configured |
X-Gitlab-Event | Human-readable event name |
X-Gitlab-Instance | GitLab instance hostname |
X-Gitlab-Webhook-UUID | Unique webhook configuration ID |
X-Gitlab-Event-UUID | Unique ID for this event delivery |
Environment Variables
GITLAB_WEBHOOK_SIGNING_TOKEN=whsec_... # "Generate signing token" in GitLab (recommended)
GITLAB_WEBHOOK_TOKEN=your_secret_token # Legacy secret token (optional)
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 gitlab --path /webhooks/gitlab
Reference Materials
- references/overview.md - GitLab webhook concepts
- references/setup.md - Configuration guide
- references/verification.md - Signature and token verification details