How to set up, verify, test, and monitor GitHub webhooks — with code examples and troubleshooting.
Last updated March 2026. Covers GitHub webhooks API v3 and GitHub Apps webhook events.
GitHub webhooks let your application receive real-time HTTP POST notifications whenever events happen in a repository or organization — a push to main, a pull request opened, an issue created, a release published. Instead of polling the GitHub API, your server gets notified the moment something happens.
Common GitHub webhook event types:
GitHub sends a ping event when you first create a webhook, which is useful for verifying your endpoint is reachable. The X-GitHub-Event header tells you which event type was delivered.
Setting up a GitHub webhook takes about 30 seconds:
application/json.openssl rand -hex 32).GitHub will immediately send a ping event to verify your endpoint is reachable. If it fails, check that your URL is publicly accessible and returns a 200 response.
GitHub signs every webhook payload using HMAC-SHA256 with your secret. The signature is sent in the X-Hub-Signature-256 header. Always verify it to ensure the request genuinely came from GitHub.
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
app.post('/webhook/github',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-hub-signature-256'];
if (!signature) {
return res.status(401).send('Missing signature header');
}
// Compute expected signature
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(req.body);
const expected = 'sha256=' + hmac.digest('hex');
// Timing-safe comparison to prevent timing attacks
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
return res.status(401).send('Invalid signature');
}
// Signature verified — safe to process
const event = req.headers['x-github-event'];
const payload = JSON.parse(req.body);
switch (event) {
case 'push':
console.log('Push to', payload.ref, 'by', payload.pusher.name);
break;
case 'pull_request':
console.log('PR', payload.action, ':', payload.pull_request.title);
break;
case 'ping':
console.log('Webhook ping received. Zen:', payload.zen);
break;
default:
console.log('Event:', event, 'Action:', payload.action);
}
res.status(200).json({ received: true });
}
);
app.listen(3000, () => console.log('GitHub webhook server on port 3000'));
Critical: use the raw request body for HMAC computation, not parsed JSON. Use crypto.timingSafeEqual to prevent timing attacks. Never use a simple === comparison for signatures.
During development, you need a public URL that GitHub can reach. Here's the fastest way:
application/json and select the events you want.ping event immediately.For local development, use sdnbros tunnel to forward webhooks from your SDNBros endpoint directly to localhost:3000.
| Error | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Signature verification failing — wrong secret or using parsed body instead of raw body | Verify your secret matches what's configured in GitHub. Use the raw request body for HMAC computation, not JSON.parse() output. |
| Timeout (no response) | Endpoint takes too long to respond — GitHub times out after 10 seconds | Return 200 immediately and process the event asynchronously. Use a message queue or background worker for heavy processing. |
| Payload too large | GitHub webhook payloads can be up to 25 MB. Your server or reverse proxy may reject large bodies. | Increase your body size limit (e.g. in nginx: client_max_body_size 30m). For very large payloads, fetch details from the API instead. |
| Webhook disabled | Too many consecutive delivery failures (GitHub disables after sustained errors) | Fix the underlying issue, then go to Settings > Webhooks and re-enable the webhook. Check "Recent Deliveries" for error details. |
| Duplicate deliveries | GitHub retries on timeout or non-2xx response. Your handler processes events more than once. | Use the X-GitHub-Delivery header (unique per delivery) to deduplicate. Make your handler idempotent. |
Create a public endpoint and inspect GitHub events. No signup is needed for the tester.
Open Webhook Tester Monitor in ProductionRelated guides: What is a Webhook? · Webhook vs API · Webhook Security · Retry Strategies · Stripe · Shopify · Slack
More: What is a Webhook? | Webhook Security Guide | Stripe Webhook Guide | Webhook vs API | Compare Tools