GitHub uses HMAC-SHA256 signatures in the X-Hub-Signature-256 header with a sha256= prefix you must strip before comparing.
application/jsonopenssl rand -hex 32Strip the sha256= prefix from the header, then hash the raw Buffer — not parsed JSON.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhooks/github', express.raw({ type: 'application/json' }), (req, res) => {
const sigHeader = req.headers['x-hub-signature-256']; // 'sha256=abc123...'
const eventType = req.headers['x-github-event']; // 'push', 'pull_request', etc.
if (!sigHeader) return res.status(400).send('Missing X-Hub-Signature-256');
// Strip prefix and compare using timing-safe function
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
.update(req.body) // raw Buffer — must NOT parse JSON first
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sigHeader), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body); // safe to parse now
switch (eventType) {
case 'ping':
// Always handle ping — GitHub sends this when webhook is first created
console.log('Webhook configured! Zen:', payload.zen);
break;
case 'push':
const branch = payload.ref.replace('refs/heads/', '');
console.log(`${payload.commits.length} commits pushed to ${branch}`);
break;
case 'pull_request':
console.log(`PR #${payload.number} ${payload.action}: ${payload.pull_request.title}`);
break;
case 'release':
console.log(`Release ${payload.release.tag_name} ${payload.action}`);
break;
default:
console.log('Unhandled event:', eventType);
}
res.status(200).json({ received: true });
});| X-GitHub-Event | Trigger |
|---|---|
| ping | Sent when webhook is first created — must return 200 |
| push | Commits pushed to any branch or tag |
| pull_request | PR opened, closed, merged, labeled, etc. |
| issues | Issue opened, closed, assigned, labeled |
| release | Release created, published, or edited |
| workflow_run | GitHub Actions workflow completed |
Testing Tip — Handle the Ping Event
GitHub sends a ping event immediately when you create a webhook. If your handler doesn't return 200 for ping, GitHub marks the webhook as failed and may auto-disable it. Always add a ping case to your switch statement.
Was this page helpful?
Your feedback helps us improve the docs.