Testing GitHub Webhooks with HookDeploy
GitHub Apps vs OAuth Apps, webhook event types, payload verification, and testing delivery with HookDeploy and ngrok.
HookDeploy Team · May 22, 2026
GitHub webhooks notify your application about repository events — pushes, pull requests, issues, releases, and dozens more. This guide covers how to set them up, verify payloads, and debug delivery failures.
GitHub Apps vs OAuth Apps vs personal tokens
| Method | Webhook support | Best for |
|---|---|---|
| GitHub App | Yes — app-level webhooks + per-repo subscriptions | Production integrations, fine-grained permissions |
| OAuth App | Limited — no webhooks on the OAuth app itself | User authorization flows, not event delivery |
| Repository webhook | Yes — configured per repo in Settings | Quick testing, repo-specific automation |
| Organization webhook | Yes — all repos in the org | Org-wide CI/CD triggers |
For most development work, start with a repository webhook pointing at HookDeploy. Graduate to a GitHub App when you need multi-repo subscriptions and installation tokens.
Common GitHub webhook events
| Event | Payload contains |
|---|---|
push | Commits, ref, pusher, repository |
pull_request | PR object, action (opened, closed, synchronize) |
issues | Issue object, action (opened, edited, closed) |
release | Release object, action (published, created) |
workflow_run | CI workflow status changes |
ping | Sent when you first create the webhook — verify delivery |
Subscribe only to events you handle. GitHub sends a ping event immediately after webhook creation — if you do not see it in HookDeploy, the URL is wrong.
Set up HookDeploy as your GitHub webhook
- Create an endpoint in HookDeploy
- Copy
https://hookdeploy.dev/h/{slug} - In your GitHub repo: Settings → Webhooks → Add webhook
- Payload URL: your HookDeploy URL
- Content type:
application/json - Secret: generate a random string — save it for verification
- Select events (start with
pushandpull_request) - Click Add webhook
GitHub sends a ping event. Check HookDeploy — you should see it within seconds.
Verify GitHub signatures
GitHub signs payloads with HMAC-SHA256 using your webhook secret. The signature is in the X-Hub-Signature-256 header:
const crypto = require('crypto');
function verifyGitHubWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your handler:
const sig = request.headers['x-hub-signature-256'];
if (!verifyGitHubWebhook(request.rawBody, sig, process.env.GITHUB_WEBHOOK_SECRET)) {
return response.status(401).send('Invalid signature');
}
Testing with ngrok vs HookDeploy
HookDeploy alone — best when your local server is not running. Capture the webhook, inspect the payload, replay later.
ngrok alone — best when your server is running and you want live traffic in your debugger.
Both together — capture in HookDeploy with forward URL set to your ngrok tunnel. You get history plus live processing.
# Terminal 1: start ngrok
ngrok http 3000
# Terminal 2: your app
npm run dev
Set HookDeploy forward URL to https://abc123.ngrok.io/webhooks/github. GitHub → HookDeploy → ngrok → localhost.
Trigger test events
Push a commit to trigger push:
git commit --allow-empty -m "test webhook"
git push
Open or close a PR to trigger pull_request. Each action generates a separate delivery with action in the payload.
Common GitHub delivery failures
Red delivery icon in GitHub Settings
GitHub shows delivery status per webhook. Click Recent Deliveries to see response codes.
| Response | Meaning |
|---|---|
| 200 | Success |
| 404 | URL wrong or HookDeploy endpoint deleted |
| 422 | HookDeploy endpoint is paused |
| 429 | HookDeploy plan limit reached |
| Timeout | GitHub waited 10 seconds — HookDeploy capture should never timeout |
Webhook not firing
- Event type not subscribed — check selected events in webhook settings
- Branch filters — org webhooks can filter by branch
- GitHub App not installed on the repo
Duplicate deliveries
- GitHub retries on non-2xx for up to 3 attempts
- Use
X-GitHub-Deliveryheader as an idempotency key
SSL errors
- HookDeploy uses valid TLS — SSL errors usually mean a typo in the URL
Inspect payloads in HookDeploy
Open a captured push event. Key fields:
{
"ref": "refs/heads/main",
"commits": [{ "id": "abc123", "message": "fix bug" }],
"repository": { "full_name": "org/repo" },
"pusher": { "name": "developer" }
}
For pull_request:
{
"action": "opened",
"number": 42,
"pull_request": {
"title": "Add feature",
"head": { "ref": "feature-branch" },
"base": { "ref": "main" }
}
}
Test with curl
curl -X POST "https://hookdeploy.dev/h/YOUR_SLUG" \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-GitHub-Delivery: test-delivery-id" \
-d '{
"ref": "refs/heads/main",
"commits": [],
"repository": { "full_name": "test/repo" }
}'
Related guides
Related guides
- Webhook security best practices
Signature verification for Stripe, GitHub, and Shopify. HMAC-SHA256, replay attacks, IP allowlisting, HTTPS, and secret rotation.
- Local webhook testing — four approaches compared
Compare ngrok, Cloudflare Tunnel, HookDeploy forwarding, and VS Code port forwarding for getting webhooks to your local dev server.
- Webhook debugging guide
Systematic approach to debugging webhook issues — delivery logs, signatures, payloads, curl testing, and common HTTP error codes.