← All guides
GitHubJuly 12, 20266 min read

Test GitHub Webhooks on Localhost (Without Deploying)

Receive GitHub webhooks on your github webhook localhost setup in minutes. Route, filter, and transform push and pull_request events with Relayers.

GitHub webhooks are easy to configure and miserable to test. The moment you add a webhook, GitHub needs a publicly reachable Payload URL — and your dev server on localhost:3000 is not that. So you end up deploying half-finished handler code to a staging box, tailing remote logs, and pushing a commit every time you want to see one more payload. That loop is slow, and it hides the thing you actually want: the raw event body and headers, on your machine, in a debugger.

This guide shows you how to receive real GitHub webhooks on localhost with Relayers. You get a stable public ingest URL, a tunnel to your dev machine, and rules to filter and reshape events before they ever hit your code. Signatures stay intact, so your X-Hub-Signature-256 verification works locally exactly like it does in production.

What you'll build

GitHub only ever talks to the stable Relayers URL. Relayers forwards matching events down the tunnel to whatever port your handler is listening on.

Prerequisites

1Create an endpoint in Relayers

Open the desktop app, go to Endpoints, and create a new one. Relayers gives you an ingest URL that looks like this:

https://api.relayers.app/v1/webhooks/wep_8fK2p...

Copy it — that's your Payload URL for GitHub. Before touching GitHub at all, hit Send test from the endpoint view to confirm the endpoint is live and shows up in your event stream.

2Add the webhook in GitHub

In your repository, go to Settings → Webhooks → Add webhook and fill in:

Save it. GitHub immediately sends a ping event to verify the URL is reachable. You should see it land in Relayers within a second or two.

Two headers matter on every delivery:

Relayers forwards both of these untouched.

3Forward events to localhost

Log in from the CLI, then start a tunnel to your local port:

relayers login
relayers listen --forward localhost:3000

Now trigger something in the repo — push a commit, open a pull request — and the event flows GitHub → Relayers → your tunnel → localhost:3000. Every delivery also shows up under Events with a full payload inspector, so you can see exactly what arrived even when your handler throws.

4Route and filter with JQ

You rarely want every event hitting your handler. Under Rules on the endpoint, add JQ-style filters. Rules evaluate top to bottom and first match wins, and you can reorder them.

To forward only newly opened pull requests, filter on the payload's action field:

{ "action": "opened" }

Anything that doesn't match openedsynchronize, closed, reopened — gets dropped before it reaches your code.

You can also split event types across different local services by adding two rules with different destinations:

Now your CI-style push handler and your PR bot run as separate processes on separate ports, each fed only the events it cares about. Destinations can also be public URLs if you want to fan out to a deployed service at the same time.

5Transform the payload

GitHub payloads are large. If your handler only needs a few fields, reshape the event in Transformations before it hits your app. Relayers supports jq, JSONata, JavaScript, and Go templates, with a test playground so you can iterate against a real captured payload.

A jq transform that flattens a pull_request event into just what you need:

{
  repo: .repository.full_name,
  pr: .number,
  title: .pull_request.title,
  author: .pull_request.user.login
}

Your handler receives a tidy object instead of a few hundred lines of JSON. Run it against a saved payload in the playground until the output looks right, then attach it to the endpoint.

A note on signatures

Because Relayers forwards the original body and headers untouched, X-Hub-Signature-256 still validates on your machine. Recompute HMAC-SHA256(secret, rawBody) in your handler and compare it against the header value, exactly as you would in production. There's no separate "local mode" to special-case — verification works locally the same way it works when deployed.

One caveat: if you apply a Relayers transform, the reshaped payload will no longer match the original signature — that's expected, since the signature covers GitHub's exact bytes. Verify against the raw body before transforming.

Going to production

When you ship, keep the endpoint. Point one rule's destination at your production URL instead of the tunnel, and the same filters and transforms apply. Use Import/Export to move the whole endpoint, rules, and transformations between environments as a JSON bundle, so your staging and prod setups stay in sync without clicking through the UI twice.

Troubleshooting

Wrap-up

You now have real GitHub webhooks landing on localhost: a stable public endpoint, a tunnel to your dev machine, JQ rules to filter and route push versus pull_request, transformations to trim the payload, and intact signatures so verification behaves identically in dev and prod. No staging deploys, no commit-and-pray loop.

Ready to stop deploying just to read a payload? Get started at relayers.app and download the app.