MergeMe logoMergeMe
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:

  1. Something happens on a merge request (opened, approved, merged, commented).
  2. Your GitLab instance sends an outbound HTTPS POST to your MergeMe webhook URL.
  3. MergeMe verifies the signing token or secret token.
  4. MergeMe checks that the project ID has a channel mapping.
  5. Workspace preferences decide whether this event should post yet.
  6. 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.

MergeMe dashboard showing GitHub, GitLab.com, self-hosted GitLab, and Slack connection status with channel and user mapping counts

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:

StatusResponse bodyMeaningGo to
200{"queued":true}Accepted. MergeMe will process itStep 5
200{"ignored":true,"reason":"unmapped_repo"}Project ID has no channel mappingStep 4
200{"ignored":true,"reason":"slack_not_connected"}Slack is not connected in this workspaceReconnect under Sources → Slack
200{"ignored":true}Event type MergeMe does not use (for example push events), or a comment not on a merge requestStep 3
401{"error":"Invalid webhook credentials"}Token mismatch or clock skewStep 2
404{"error":"Workspace not found"}Wrong workspace ID in the webhook URLRecopy the URL from Sources → Self-hosted
No response / timeoutn/aGitLab cannot reach MergeMeStep 0

A 200 does not always mean "it worked". MergeMe returns 200 for 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:

ValueGitLab fieldGitLab 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:

SymptomMissing trigger
No card at all, 200 {"ignored":true} in Recent eventsMerge request events
Card posts, but comments never appear in the threadComments / Note events
Card posts, but the CI line never showsPipeline 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.

Step 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.

  1. 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.
  2. Phrase trigger is on. If preferences use "Post when a phrase is commented", nothing posts until someone comments the exact phrase.
  3. The author is excluded. Excluded usernames under Routing → Preferences silently drop that account's MRs and comments.
  4. 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).
  5. 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 webhooksApplication mode
Who registers webhooksYou, per project or per groupMergeMe, automatically
Project and label pickersManual entry (project ID, typed labels)Dropdowns from your instance
GitLab → api.mergeme.dev outboundRequiredRequired
MergeMe → your GitLab inboundNot requiredRequired (HTTPS)
Stale PR/MR remindersNot supportedSupported

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-mrs
  • acme/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

Work the checklist top to bottom, read the response body, and the silent webhook usually explains itself in a minute.