GitLab Integration
Receive and route GitLab webhooks for pushes, merge requests, issues, notes and pipelines.
Warning
This provider is not a signature scheme. GitLab's secret token mode sends the token you configured
back to you as plain text in the X-Gitlab-Token header, and Hookbase compares that header value with
the signingSecret stored on the source. Nothing is hashed, and the token says nothing about the body
it arrived with — it authenticates the sender, not the payload.
Treat it as a bearer credential: anyone who learns the token can post any payload they like to your ingest URL and it will compare equal. Use a long random value, keep it out of logs, and rotate it on both sides at once.
Setup
1. Create a Source in Hookbase
Unlike the HMAC providers, you choose this secret yourself. Generate a long random value and use the same string on both sides.
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "GitLab Production",
"slug": "gitlab",
"provider": "gitlab",
"signingSecret": "a-long-random-secret-token"
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/gitlab2. Create the GitLab Webhook
- Open the project's (or group's) settings and go to its webhooks
- Set the URL to your Hookbase ingest URL
- Paste the same value you stored as
signingSecretinto the webhook's secret token field - Select the trigger events you want delivered
- Save, then use GitLab's test delivery to confirm the token matches before you rely on it
See GitLab's webhook documentation for the trigger list.
3. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Pipeline Bridge", "slug": "gitlab-bridge", "url": "https://api.myapp.com/webhooks/gitlab"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "GitLab to Pipeline Bridge", "sourceId": "src_...", "destinationId": "dst_..."}'A route carries one destination. Fanning one source out to several endpoints is one route each.
Signature Verification
There is no signature to verify. GitLab echoes the secret token:
X-Gitlab-Token: a-long-random-secret-tokenHookbase compares that header value with the source's signingSecret in constant time — the token is
a credential, so the comparison is timing-safe even though no hash is involved — and records the result
as signature_valid exactly as it would for an HMAC provider. No algorithm, encoding, prefix or signing
string applies to this mode, because nothing is hashed:
{
"provider": "gitlab",
"signingSecret": "a-long-random-secret-token"
}Two consequences of there being no hash are worth planning for:
- The token does not cover the body. A valid token on a tampered payload still compares equal. Anything you would normally infer from a valid signature — that the bytes are unmodified — does not follow here.
- There is no timestamp, so no clock tolerance applies. The scheme offers no replay protection of its own. Hookbase's own deduplication is what collapses a repeated delivery; see below.
The comparison is against the whole header value, byte for byte. There is no prefix to strip and no trimming, so a trailing space or newline in either the GitLab field or the stored secret makes every delivery fail to verify.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have requests with a missing or wrong token refused with 401 instead of stored. It defaults
to false, which is why a mistyped token shows up first as an unverified badge rather than as lost
traffic.
Signing Token Mode
GitLab also offers a newer signing token mode, which is the Standard Webhooks specification
verbatim: webhook-signature, webhook-id and webhook-timestamp, with a real HMAC over the id, the
timestamp and the body.
A webhook configured that way sends no X-Gitlab-Token, so this provider has nothing to compare and
everything arrives unverified. Select the standard-webhooks provider on that source instead — see
Standard Webhooks for the header list, the timestamp
tolerance and the whsec_ secret handling.
Common Events
GitLab names the event in the X-Gitlab-Event header and repeats a machine-readable form in the body
as object_kind:
X-Gitlab-Event | object_kind | Description |
|---|---|---|
Push Hook | push | Commits were pushed to a branch |
Tag Push Hook | tag_push | A tag was pushed |
Issue Hook | issue | An issue was opened, updated or closed |
Merge Request Hook | merge_request | A merge request changed state |
Note Hook | note | A comment was added |
Pipeline Hook | pipeline | A pipeline changed state |
Job Hook | build | A job changed state |
Release Hook | release | A release was created or updated |
Hookbase reads the event type for a GitLab source out of the X-Gitlab-Event header, so events are
recorded under the names in the first column — Push Hook, not push. Filters, which read the
payload, see object_kind instead.
Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
Push Summary
function transform(payload) {
const project = payload.project || {};
return {
kind: payload.object_kind || null,
project: project.path_with_namespace || null,
project_url: project.web_url || null,
ref: payload.ref || null,
checkout_sha: payload.checkout_sha || null,
pushed_by: payload.user_username || null,
commit_count: payload.total_commits_count != null
? payload.total_commits_count
: (payload.commits || []).length,
received_at: new Date().toISOString()
};
}Slack Alert for Merge Requests
function transform(payload) {
const mr = payload.object_attributes || {};
const project = payload.project || {};
return {
text: `Merge request ${mr.action || "updated"}: ${mr.title || mr.iid || "unknown"}`,
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Project:*\n${project.path_with_namespace || "unknown"}` },
{ type: "mrkdwn", text: `*Title:*\n${mr.title || "unknown"}` },
{ type: "mrkdwn", text: `*Branch:*\n${mr.source_branch || "?"} to ${mr.target_branch || "?"}` },
{ type: "mrkdwn", text: `*State:*\n${mr.state || "unknown"}` }
]
}
]
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
Pushes Only
{
"name": "Pushes Only",
"logic": "AND",
"conditions": [
{
"field": "object_kind",
"operator": "equals",
"value": "push"
}
]
}Merge Requests That Opened or Merged
{
"name": "Merge Request Opened or Merged",
"logic": "OR",
"conditions": [
{
"field": "object_attributes.action",
"operator": "equals",
"value": "open"
},
{
"field": "object_attributes.action",
"operator": "equals",
"value": "merge"
}
]
}One Project
{
"name": "Web App Project",
"logic": "AND",
"conditions": [
{
"field": "project.path_with_namespace",
"operator": "equals",
"value": "acme/web-app"
}
]
}Headers
| Header | Description |
|---|---|
X-Gitlab-Token | The secret token, as plain text. Compared with signingSecret; not a signature |
Hookbase also reads two GitLab headers that are not part of verification: X-Gitlab-Event, which
becomes the event type and is stored with the event, and X-Gitlab-Delivery, which is used for
deduplication. Neither is replayed to your destination — a delivery to an HTTP destination carries
Hookbase's own X-Event-ID and X-Delivery-ID headers plus whatever the destination is configured to
add — so anything your handler needs should come out of the payload.
Troubleshooting
Token Comparison Failed
- Confirm the source's
providerisgitlab— a source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256, none of which GitLab sends - Confirm
signingSecretand GitLab's secret token field hold the same string, with no trailing whitespace on either side. The whole header value is compared, so a stray newline is a mismatch - Confirm the webhook is in secret token mode. If it was configured with a signing token instead,
there is no
X-Gitlab-Tokento compare and the source needs thestandard-webhooksprovider - Check the project. A group webhook and a project webhook each have their own token
No Events Arriving
- Check the webhook URL matches your ingest URL exactly, org and source slugs included
- Check the trigger events selected on the webhook cover what you are doing — push events do not fire for a merge request, and vice versa
- Use GitLab's test delivery and read the response it shows you. A
401there means the token did not compare equal and the source hasrejectInvalidSignatureson - Check GitLab has not disabled the webhook after repeated failures
Duplicate Events
GitLab sends a unique delivery id on every request, and on a source with dedupEnabled: true the
default auto strategy uses it: it reads X-Gitlab-Delivery and falls back to hashing the payload only
when that header is absent. Because the token does not cover the body, that id is not tamper-proof the
way a signed message id is — it is a convenience against GitLab retrying, not a defence against a
replayer. Set dedupStrategy to payload_hash if you would rather key on the body itself.
A source with a signing secret also gets a 24-hour payload-hash replay check on every request that
compared equal, whether or not dedupEnabled is on. That is the only replay protection in play here,
since the scheme carries no timestamp.