The GitHub Webhook Testing Problem
GitHub webhooks power a huge range of automations: CI/CD triggers, PR bots, auto-labellers, deploy hooks, Slack notifications, changelog generators. All of them share one constraint: GitHub needs a public HTTPS URL to deliver events.
During development, your handler runs locally at something like localhost:3000. GitHub cannot reach that. The usual workarounds:
- ngrok — creates a public tunnel to your port, but requires installing and authenticating a binary, running a persistent terminal process, and updating GitHub every time the URL rotates (free plan)
- Deploy to staging — painfully slow iteration loop; you can't breakpoint a deployed handler
- GitHub's "Redeliver" button — re-sends the last event, but only shows up after the first delivery, and you still need a public URL
There's a faster path. This guide walks through the complete setup for local GitHub webhook testing using a cloud-side relay that requires nothing to install.
What You Need
- A GitHub account and repository (or GitHub App)
- A local server running your webhook handler (e.g.
localhost:3000) - A free WebhookWhisper account — or use guest mode with no account at all
No binary installs, no CLI setup, no persistent tunnel process.
Step 1 — Get a Public Endpoint
Go to webhookwhisper.com and click Get my free webhook URL. You get an HTTPS URL like:
https://webhookwhisper.com/hook/abc123xyz
This is live immediately. No configuration, no account required for the guest endpoint.
If you're working on an integration you'll develop over days or weeks, sign up for a free account to get a permanent URL that doesn't expire. You register it with GitHub once and never touch that setting again.
Step 2 — Add the URL to GitHub
In your GitHub repository:
- Go to Settings → Webhooks → Add webhook
- Paste your WebhookWhisper URL in the Payload URL field
- Set Content type to
application/json - Add a Secret if you want to test signature verification (optional)
- Under Which events, choose Send me everything or select specific events
- Click Add webhook
GitHub immediately fires a ping event to verify your URL. You'll see it arrive in the WebhookWhisper inspector within a second.
You can also fire realistic GitHub payloads instantly without a real repo using the GitHub webhook testing page.
Step 3 — Forward Events to Your Local Handler
- In your WebhookWhisper endpoint, open the Forwarding tab
- Click Add rule
- Set the target URL to your local handler:
http://localhost:3000/webhooks/github - Click Save
Now every GitHub event that hits your WebhookWhisper URL is automatically relayed to your local server — with all original headers intact, including:
X-GitHub-Event— the event type (push, pull_request, etc.)X-Hub-Signature-256— HMAC-SHA256 signature of the payloadX-GitHub-Delivery— unique delivery ID for deduplication
Step 4 — Verify GitHub Webhook Signatures
If you added a secret in Step 2, GitHub signs every payload with it. Here's how to verify the signature in Node.js:
import crypto from 'crypto'
import express from 'express'
const app = express()
// ⚠️ Use raw body — signature check fails if body is already parsed
app.post('/webhooks/github',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-hub-signature-256']
const githubEvent = req.headers['x-github-event']
const deliveryId = req.headers['x-github-delivery']
// Verify signature
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('Signature mismatch')
}
const payload = JSON.parse(req.body.toString())
// Route by event type
switch (githubEvent) {
case 'push':
console.log(`Push to ${payload.ref} by ${payload.pusher.name}`)
// trigger CI, invalidate cache, etc.
break
case 'pull_request':
if (payload.action === 'opened') {
console.log(`PR #${payload.number} opened: ${payload.pull_request.title}`)
}
break
case 'workflow_run':
console.log(`Workflow ${payload.workflow.name}: ${payload.action}`)
break
}
res.json({ received: true })
}
)
Since WebhookWhisper forwards all headers intact, X-Hub-Signature-256 arrives at your local handler exactly as GitHub sent it. The signature verification works identically to production.
Step 5 — Trigger Events and Test Your Handler
With forwarding active, trigger events naturally:
- push event: push a commit to the repo
- pull_request event: open or update a PR
- workflow_run event: trigger a GitHub Actions workflow
- issues event: create or close an issue
Each event appears in the WebhookWhisper inspector immediately. You see the full pretty-printed payload and all headers. Simultaneously, WebhookWhisper forwards the event to your local handler and logs the HTTP status your handler returned.
If your handler returns 500, WebhookWhisper shows it in the delivery log immediately — no need to dig through server logs to know whether it succeeded.
Replaying Events After Fixing a Bug
This is where WebhookWhisper's approach beats every alternative. Fix the bug in your handler, restart your local server, then click Replay on any past event in the WebhookWhisper inspector.
The exact payload is re-delivered to your handler immediately — no need to push another commit, open another PR, or wait for GitHub to retry. The entire fix-and-retest cycle takes seconds.
Testing GitHub App Webhooks
GitHub Apps receive events from all installed repositories through a single webhook URL configured on the App itself. The process is the same:
- In your GitHub App settings, set the Webhook URL to your WebhookWhisper endpoint
- Add a forwarding rule to your local handler
- All installation events, repository events, and check runs appear in the inspector and are forwarded to your local server
During active GitHub App development, you can leave this setup permanently. The WebhookWhisper URL is stable — you never update the App settings again.
GitHub Webhook Events Reference
Here are the most commonly tested GitHub webhook events and what they're used for:
| Event | When it fires | Common use case |
|---|---|---|
push | Commits pushed to any branch | Trigger CI, invalidate caches |
pull_request | PR opened, closed, merged, reviewed | Auto-assign, status checks |
pull_request_review | Review submitted on a PR | Auto-merge on approval |
workflow_run | Actions workflow completes | Post-deploy hooks, notifications |
issues | Issue opened, closed, labelled | Sync to Linear/Jira, triage |
issue_comment | Comment posted on issue or PR | Bot slash commands |
release | Release published or created | Trigger deploy, update changelog |
create | Branch or tag created | Auto-provision environments |
check_run | CI check completes | Update PR status, notifications |
star | Repo starred/unstarred | Track growth metrics |
Common Gotchas
Content type must be application/json
When adding a webhook in GitHub settings, the Content type dropdown defaults to application/x-www-form-urlencoded. Change it to application/json. If you leave it as form-encoded, your handler receives a URL-encoded string instead of JSON.
Deduplication with X-GitHub-Delivery
GitHub guarantees at-least-once delivery. If your endpoint returns 5xx, GitHub retries — meaning your handler can receive the same event twice. Use the X-GitHub-Delivery header value as an idempotency key to detect and skip duplicate deliveries.
Different payloads for the same event type
The pull_request event has an action field that can be opened, closed, merged, synchronize, labeled, and more. Always check the action field before acting — otherwise your "PR merged" handler will fire on every PR update. WebhookWhisper's payload inspector makes it easy to compare payloads for different action values side by side.
Related Guides
- How to Debug Webhooks: A Practical Guide
- Forward Webhooks to Localhost Without ngrok
- GitHub Webhook Events & Payload Reference
- GitHub Webhook Testing — Free Tool
Summary: The Full Local Testing Workflow
Once set up, your GitHub webhook test loop looks like:
- Start your local handler:
npm run dev - Make a code change to your handler
- Push a commit (or open a PR, or trigger whatever event you're handling)
- Watch the event arrive in the WebhookWhisper inspector
- Check the delivery log — did your handler return 200?
- If not, fix the bug and click Replay
No deploy. No staging server. No ngrok binary. Just fast, local iteration on real GitHub payloads.
Create your free WebhookWhisper account and have your first GitHub webhook flowing to localhost in under two minutes.