Jenkins Webhooks: The Complete Setup Guide (GitHub, Bitbucket, GitLab, Gitea)

Every Jenkins webhook URL in one place — /github-webhook/, /bitbucket-hook/, /project/NAME, /gitea-webhook/post — plus how to fix 403 no valid crumb and receive webhooks when Jenkins has no public IP.

Jenkins webhooks setup guide

Jenkins can start a build the instant someone pushes, instead of waking up every few minutes to poll. That is what webhooks are for. But almost every Jenkins webhook question comes down to one of three things: which URL do I point the provider at, why am I getting a 403, and how does any of this work when Jenkins sits on a private network with no public IP.

This guide answers all three, for GitHub, Bitbucket, GitLab and Gitea.

Jenkins webhook URLs, all in one place

Each Jenkins plugin registers its own endpoint. Point your provider at the matching one:

ProviderJenkins webhook URLPlugin
GitHubhttp://JENKINS_URL/github-webhook/GitHub plugin
Bitbuckethttp://JENKINS_URL/bitbucket-hook/Bitbucket plugin
GitLabhttp://JENKINS_URL/project/PROJECT_NAMEGitLab plugin
Giteahttp://JENKINS_URL/gitea-webhook/postGitea plugin
Anything elsehttp://JENKINS_URL/generic-webhook-trigger/invokeGeneric Webhook Trigger

Three details cause most of the failures:

  1. The trailing slash on /github-webhook/ and /bitbucket-hook/ is required. Drop it and you are no longer hitting the plugin's CSRF-exempt endpoint, which surfaces as a 403.
  2. GitLab's URL contains the job name, not a fixed path — /project/my-job, or /project/my-folder/my-job for a job inside a folder. Pointing GitLab at /job/PROJECT_NAME/build skips the plugin and will not behave as documented.
  3. JENKINS_URL is whatever Jenkins is reachable at, including any context path. If Jenkins runs under /jenkins, the GitHub endpoint is http://host/jenkins/github-webhook/.

The problem: Jenkins usually is not on the public internet

Jenkins has broad access to your infrastructure, so it normally lives behind a firewall, inside a VPC, or on a NAT'd network. That is the right call for security — and it is exactly what breaks webhooks, because GitHub and friends can only POST to a public URL.

The usual workarounds are all bad: exposing Jenkins to the internet, maintaining IP allowlists that the provider keeps changing, or falling back to polling and accepting the delay plus the API rate limiting that comes with it.

Webhook Relay solves it from the other direction. You run a small agent next to Jenkins; it makes an outbound connection and streams incoming webhooks to your internal Jenkins URL. The provider only ever sees a public Webhook Relay endpoint.

Receiving GitHub webhooks on Jenkins without a public IP

A few properties that matter for a team setup:

  • No inbound ports. Nothing about your firewall changes.
  • One agent, many Jenkins servers. A single agent can relay to any number of internal hosts it can reach.
  • The public endpoint is stable. Reprovision the machine behind it and the provider configuration stays the same.
  • Webhooks stay uni-directional. Responses are not returned to the sender, so Jenkins is not probeable through the endpoint.

Setting it up

Install the agent and create a forwarding rule pointing at the endpoint for your provider:

# install (Linux x86-64; see the docs for other platforms)
sudo wget -O /usr/local/bin/relay \
  https://storage.googleapis.com/webhookrelay/downloads/relay-linux-amd64
sudo chmod +wx /usr/local/bin/relay

# authenticate with a token key/secret from https://my.webhookrelay.com/tokens
relay login -k YOUR_TOKEN_KEY -s YOUR_TOKEN_SECRET

# create the public endpoint and start forwarding
relay forward --bucket jenkins http://localhost:8080/github-webhook/

That prints a public endpoint:

https://vlndyzsibcil98gdte7yp1.hooks.webhookrelay.com -> http://localhost:8080/github-webhook/

Put that URL into your provider's webhook settings. Swap the destination for /bitbucket-hook/, /project/my-job or /gitea-webhook/post depending on which provider you are wiring up.

For anything long-lived, run the agent as a service so it survives reboots — pass --no-agent to create the configuration without starting a foreground agent, then install it via Docker (--restart always) or as a background OS service. Both are covered step by step in the Jenkins and GitHub tutorial.

Per-provider walkthroughs

The full production setups, with screenshots and shared-secret configuration, live in the docs:

Troubleshooting

403 No valid crumb was included in the request

The request landed on a Jenkins path that is not exempt from CSRF protection. Check, in order:

  1. Trailing slash. /github-webhook/, not /github-webhook.
  2. An extra path segment. Some integrations append a path (/ghprbhook, for instance) to whatever destination you configured, producing a final URL Jenkins does not recognise. Look at the actual request path in your Webhook Relay logs and make sure it is exactly the endpoint you expect.

400 Signature was expected, but not provided

Jenkins is configured with a shared secret but the incoming request has no signature — the secret is missing on the provider side, or the two do not match. Set the same secret in both places. On GitHub that is the webhook's Secret field; in Jenkins it goes under the GitHub section → AdvancedShared secrets.

Jenkins shared secret configuration

Generate one with openssl rand -base64 32.

Jenkins returns 200 but nothing builds

Jenkins accepted the webhook and could not match it to a job. It reads the repository URL out of the payload and looks for a job configured against that repository, so the URL in your job's Source Code Management section has to match exactly. Check the Jenkins system log for No SCM configuration was found!.

Everything shows as "received" but never "forwarded"

The agent is not running. Webhook Relay stored the request but had nothing to deliver it to. Start the agent — and if this happens intermittently, install it as a service rather than running it in a shell.

Which approach should you use?

SituationUse
Jenkins has a public IP and you are happy exposing itProvider webhook straight to the Jenkins endpoint
Jenkins is behind a firewall, NAT or in a private networkWebhook Relay agent forwarding to the internal URL
You would rather not run a separate agentJenkins plugin subscribing to a bucket
Jenkins runs in KubernetesAgent as a sidecar
Provider has no Jenkins pluginGeneric Webhook Trigger at /generic-webhook-trigger/invoke

Wrapping up

Most Jenkins webhook problems are URL problems. Get the endpoint exactly right — trailing slash included — make sure the repository in the job matches the repository in the payload, and set the shared secret on both sides.

The part that genuinely needs infrastructure is receiving webhooks at all when Jenkins is not publicly reachable, which is the normal case for a company CI server. An outbound agent handles that without opening a single port. Start forwarding for free.