- Published on
Self-hosted GitLab Slack webhook not working? A troubleshooting checklist
- Authors

- Name
- Maruan
You added the webhook, opened a test merge request on your self-hosted GitLab, and nothing showed up in Slack. No card, no error, just silence. This checklist walks through the causes we see most often, in the order that finds them fastest.
MergeMe posts GitLab merge requests and GitHub pull requests to Slack as one updating card per PR/MR, with review comments as thread replies. GitLab.com and GitHub.com register webhooks for you. Self-hosted GitLab with manual webhooks is the one setup where you paste a URL and token into GitLab yourself, so it is also where most "why is nothing posting?" questions come from.
How a self-hosted GitLab event reaches Slack
Knowing the path makes each check obvious:
- Something happens on a merge request (opened, approved, merged, commented).
- Your GitLab instance sends an outbound HTTPS
POSTto your MergeMe webhook URL. - MergeMe verifies the signing token or secret token.
- MergeMe checks that the project ID has a channel mapping.
- Workspace preferences decide whether this event should post yet.
- The Slack card is created or updated in place.
Each step below maps to one of those stages. Work top to bottom and stop when you find the problem.

Step 1: Read the webhook response in GitLab
Before changing anything, look at what GitLab actually received back. In GitLab, open the project or group, go to Settings → Webhooks, edit the MergeMe webhook, and scroll to Recent events. Each delivery shows the HTTP status and response body.
The response body tells you which stage failed:
| Status | Response body | Meaning | Go to |
|---|---|---|---|
200 | {"queued":true} | Accepted. MergeMe will process it | Step 5 |
200 | {"ignored":true,"reason":"unmapped_repo"} | Project ID has no channel mapping | Step 4 |
200 | {"ignored":true,"reason":"slack_not_connected"} | Slack is not connected in this workspace | Reconnect under Sources → Slack |
200 | {"ignored":true} | Event type MergeMe does not use (for example push events), or a comment not on a merge request | Step 3 |
401 | {"error":"Invalid webhook credentials"} | Token mismatch or clock skew | Step 2 |
404 | {"error":"Workspace not found"} | Wrong workspace ID in the webhook URL | Recopy the URL from Sources → Self-hosted |
| No response / timeout | n/a | GitLab cannot reach MergeMe | Step 0 |
A
200does not always mean "it worked". MergeMe returns200for events it deliberately ignores, so GitLab does not treat them as failures. Always read the body.
You can replay any delivery with Resend request after fixing the cause, instead of opening a new merge request.
Step 0: Check outbound network access
If Recent events shows timeouts, connection errors, or SSL errors, the request never reached MergeMe. Self-hosted GitLab cannot be fully air-gapped: it must make outbound HTTPS requests to api.mergeme.dev on port 443. With manual webhooks, MergeMe never connects into your network.
Test from the GitLab server itself, not from your laptop:
curl -sS https://api.mergeme.dev/health
# Expected: {"ok":true}
If that fails, check in this order:
- Firewall egress rules on the GitLab host or its network
- Outbound proxy. If your network forces traffic through a proxy, GitLab's Rails process needs to know about it
- TLS inspection. A corporate proxy that rewrites certificates can make GitLab reject the connection. Leave SSL verification enabled on the webhook and fix trust at the proxy instead
For Linux package (Omnibus) installs behind a proxy, the proxy is usually set in /etc/gitlab/gitlab.rb, followed by gitlab-ctl reconfigure:
gitlab_rails['env'] = {
"http_proxy" => "http://proxy.example.com:8080",
"https_proxy" => "http://proxy.example.com:8080",
"no_proxy" => "localhost,127.0.0.1"
}
Check the GitLab documentation for your install type (Helm, Docker, source) before changing proxy settings on a production instance.
Step 2: Fix a 401 (tokens and clocks)
A 401 means MergeMe received the request but could not verify it. Three causes cover almost every case.
Token pasted into the wrong field
MergeMe shows two values under Sources → Self-hosted:
| Value | GitLab field | GitLab version |
|---|---|---|
| Signing token | "Signing token" | 19.0 and later (recommended) |
| Secret token | "Secret token" | All versions (fallback) |
Pasting the signing token into the secret token field (or the reverse) fails verification. MergeMe checks the signing token first and falls back to the secret token automatically, so you only need one of them, in the matching field.
Tokens were regenerated
Regenerate tokens in MergeMe invalidates the old values immediately. Every GitLab webhook that used them, project or group, must be updated with the new token. If one project works and another returns 401, an old token on the second webhook is the usual cause.
Server clock drift (signing token only)
The signing token includes a timestamp, and MergeMe rejects requests more than 5 minutes away from its own clock. This is replay protection. A GitLab server whose clock has drifted will get 401 on every delivery even with a perfect token.
timedatectl status
# Look for: System clock synchronized: yes
If the clock is not synchronised, fix NTP on the host. The secret token has no timestamp check, which is why switching to it can appear to "fix" the problem while hiding the drift.
Step 3: Confirm the right events are enabled
MergeMe listens for three GitLab webhook triggers:
- Merge request events: required for the card itself (opened, approved, merged, closed)
- Comments (shown as Note events on some GitLab versions): required for thread replies
- Pipeline events: only needed if you turn on Show CI status on Slack cards
Symptoms map directly:
| Symptom | Missing trigger |
|---|---|
No card at all, 200 {"ignored":true} in Recent events | Merge request events |
| Card posts, but comments never appear in the thread | Comments / Note events |
| Card posts, but the CI line never shows | Pipeline events (and the preference must be on) |
Push events, tag events, and issue events are ignored, so enabling extra triggers is harmless but adds traffic.
Get self-hosted GitLab merge requests and GitHub pull requests into Slack withone updating card per PR/MR from MergeMe
Start freeStep 4: Check the project ID mapping
A 200 with "reason":"unmapped_repo" means the event arrived and verified, but the project is not mapped. MergeMe deliberately drops events from unmapped projects so Slack only shows repos you chose.
- Find the numeric Project ID in GitLab under the project's Settings → General
- Compare it with the row in Routing → Channel mappings in MergeMe
- Use the numeric ID, not the project path (
group/project)
Group webhooks (GitLab Premium or Ultimate) send events for every project in the group and its subgroups. Seeing unmapped_repo for projects you did not map is expected and correct. Only mapped project IDs post to Slack.
Step 5: Accepted, but still no Slack card
If Recent events shows {"queued":true} and Slack is still empty, the event reached MergeMe and the remaining causes are preferences and Slack, not GitLab.
- The MR is a draft. The default When to post setting is "Post when ready for review", so draft and WIP merge requests wait until they are marked ready.
- Phrase trigger is on. If preferences use "Post when a phrase is commented", nothing posts until someone comments the exact phrase.
- The author is excluded. Excluded usernames under Routing → Preferences silently drop that account's MRs and comments.
- Label routing with Prevent default posting. If the mapping only posts labelled MRs and none of the rules match at first post, nothing is sent. With manual webhooks, label names must be typed exactly as they appear in GitLab (matching ignores case).
- Private Slack channel. MergeMe can post to public channels without an invite. Private channels only appear in the channel picker after the MergeMe app is added to them.
The preferences guide covers draft, phrase, and bot settings in detail.
Copy-paste checklist
[ ] curl https://api.mergeme.dev/health from the GitLab host returns {"ok":true}
[ ] Webhook URL copied from Sources → Self-hosted (correct workspace ID)
[ ] Signing token in "Signing token" field (19.0+) OR secret token in "Secret token" field
[ ] Webhook updated after any token regeneration
[ ] GitLab host clock synchronised (signing token)
[ ] Merge request events + Comments/Note events enabled
[ ] Pipeline events enabled if CI status on cards is on
[ ] Numeric project ID matches the channel mapping row
[ ] MR is not a draft (or When to post is set to "Post when opened")
[ ] Author is not on the excluded usernames list
[ ] MergeMe app invited to the Slack channel if it is private
Manual webhooks vs Application mode
Self-hosted GitLab can also connect in Application mode, which works like GitLab.com: you create a GitLab Application, MergeMe lists projects and labels for you, and webhooks are registered automatically. The trade-off is network direction.
| Manual webhooks | Application mode | |
|---|---|---|
| Who registers webhooks | You, per project or per group | MergeMe, automatically |
| Project and label pickers | Manual entry (project ID, typed labels) | Dropdowns from your instance |
GitLab → api.mergeme.dev outbound | Required | Required |
| MergeMe → your GitLab inbound | Not required | Required (HTTPS) |
| Stale PR/MR reminders | Not supported | Supported |
If your instance is not reachable from the internet, manual webhooks are the right choice and every step above applies.
Using GitHub alongside self-hosted GitLab
Many self-hosted GitLab teams also have repos on GitHub.com, often open-source libraries or a frontend. Both can live in the same MergeMe workspace. The GitHub App registers its own webhooks, so none of the self-hosted checks above apply to the GitHub side, and each mapping is tagged by git source in the dashboard.
A typical mixed setup:
platform/billing-service(self-hosted GitLab) →#platform-mrsacme/web-app(GitHub.com) →#frontend-reviews
See mixed GitHub and GitLab teams for how that looks in practice, or the GitHub setup guide to add it.
FAQ
Why does my self-hosted GitLab webhook return 200 but nothing posts to Slack?
Because MergeMe returns 200 for events it deliberately ignores. Check the response body in Recent events: unmapped_repo means the project ID is not mapped, and {"queued":true} means the event was accepted and a preference (draft status, excluded user, label rules) is holding it back.
Can MergeMe work with an air-gapped GitLab instance?
No. GitLab must make outbound HTTPS requests to api.mergeme.dev on port 443 to deliver webhooks. With manual webhooks, MergeMe never connects into your network.
Should I use the signing token or the secret token?
Use the signing token if your GitLab is on 19.0 or later. It signs the request body with HMAC-SHA256 and includes a timestamp for replay protection. The secret token is a plain header value for older versions.
Do I need a webhook for every self-hosted GitLab project?
Not necessarily. On GitLab Premium or Ultimate, one group webhook covers every project in the group and its subgroups. Map only the project IDs you want in MergeMe; the rest are ignored.
Does MergeMe support both GitHub and GitLab in one workspace?
Yes. Connect GitHub.com, GitLab.com, and self-hosted GitLab in the same workspace. Each channel mapping and user mapping is tagged by git source.
Does MergeMe read my source code?
No. MergeMe processes webhook payloads (merge request metadata, labels, comments, and optional pipeline status). It does not clone or read your repositories.
Get started
- Self-hosted GitLab setup: manual webhooks, Application mode, and network requirements
- Webhook reference: URLs, headers, and supported events
- Start free: self-hosted GitLab setup takes about ten minutes per project
Work the checklist top to bottom, read the response body, and the silent webhook usually explains itself in a minute.