Receive GitHub Webhooks Locally (Test GitHub Webhooks on localhost)
Receive GitHub webhooks locally and test them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.
You are building a GitHub integration — a bot, a CI trigger, a deploy hook — and you need to see your handler react to a real push or pull_request event. The problem is immediate: GitHub will only POST to a public URL, and your handler is running on localhost:8080. GitHub has no way to reach it.
The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting payloads from the docs into curl gives you a guess at the real request, not the real headers and body GitHub actually sends. What you want is to receive GitHub webhooks locally — real events, hitting your local handler, with a URL that does not change every time you restart.
This guide shows how to do exactly that.
Why receiving GitHub webhooks locally is tricky
A webhook is just an HTTP request that GitHub sends to a URL when something happens in your repo. GitHub sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open.
So you need something in the middle: a public endpoint GitHub can hit, that relays each request down to your laptop without you opening a single firewall port. That is what Webhook Relay does — and unlike a random tunnel URL, the endpoint is stable, so you configure GitHub once and never touch it again.
Step 1: Inspect the real payload with Webhook Bin
Before you write any handler code, find out what GitHub actually sends. Open the free Webhook Bin — no signup — and you get an instant public URL.
- Copy the Webhook Bin URL.
- In your repository, go to Settings → Webhooks → Add webhook.
- Paste the URL into Payload URL, set Content type to
application/json, and choose the events you care about (or "Send me everything"). - Click Add webhook.
GitHub immediately fires a ping event to confirm the endpoint works — you will see it land in Webhook Bin right away. Trigger a real action (push a commit, open a pull request) and inspect the captured request: the full JSON body, the query string, and every header, including X-GitHub-Event, X-GitHub-Delivery, and X-Hub-Signature-256.
Now you know the exact shape of the data before writing a line of code. For more on this approach, see How to test webhooks and What is a webhook.
Step 2: Forward the events to localhost with the relay agent
Once you know the payload, route those same events into your local handler. Sign up for Webhook Relay, install the relay agent (CLI or Docker), and create a bucket — say github. The bucket gives you a stable public input endpoint.
Start forwarding to your local server:
relay forward --bucket github http://localhost:8080/webhook
The agent opens an outbound connection to Webhook Relay and streams every incoming request down to http://localhost:8080/webhook. Because the connection is outbound, there are no firewall ports to open and no public IP needed — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official webhookrelay/webhookrelayd image. Full details are in the localhost forwarding docs.
Now update the GitHub webhook's Payload URL to your Webhook Relay endpoint (or just create it there from the start). Push a commit and watch it arrive on localhost.
GitHub-specific configuration and quirks
A few GitHub details worth knowing:
- Where to add it: repository Settings → Webhooks for a single repo, or organization Settings → Webhooks to cover every repo in the org.
- Content type: use
application/jsonso the body is raw JSON. (The alternative,application/x-www-form-urlencoded, wraps the JSON inside apayloadform field — easy to trip over.) - Events: pick individual events (push, pull_request, issues, release, …) or choose "Send me everything." Use the
X-GitHub-Eventheader to branch in your handler. - The ping event: GitHub sends a
pingthe moment you create the webhook. It is a great sanity check that the endpoint is reachable. - Redeliver: under Settings → Webhooks → Recent Deliveries, every past delivery can be re-sent with the original payload and headers. This is the single most useful feature for local development — keep your
relay forwardrunning and replay a real delivery as many times as you need.
Step 3: Verify the GitHub webhook signature
If you set a Secret on the webhook, GitHub signs each request. It computes an HMAC-SHA256 of the raw request body using your secret and sends the digest in the X-Hub-Signature-256 header (formatted as sha256=...). Your handler should recompute the HMAC over the raw body and compare in constant time before trusting the payload.
To sanity-check your implementation, paste a captured body, your secret, and the received signature into the free HMAC signature verifier. For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read Verify a webhook signature.
Replay and iterate
This is where local development gets fast:
- Replay from GitHub via the Redeliver button to re-run a real delivery against your handler.
- Replay from Webhook Relay — past requests are stored on your bucket, so you can resend a captured event without touching GitHub at all.
- Iterate on your handler by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path.
Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the GitHub configuration never needs to change.
Get started
- Inspect the real payload in the free Webhook Bin — no signup needed.
- Create a Webhook Relay account, install the agent, and run
relay forward --bucket github http://localhost:8080/webhook. - Point your GitHub webhook at the stable endpoint, trigger an event, and watch it hit
localhost.
You will be testing real GitHub events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once.
