GitHub signs each delivery with HMAC-SHA256 over the raw payload, using your webhook secret. It sends the result as sha256=<hex> in the X-Hub-Signature-256 header. When your computed value doesn't match, the cause is usually one of these:
X-Hub-Signature-256 if the webhook has no secret, so your code is comparing against nothing.X-Hub-Signature is legacy HMAC-SHA1. Use the -256 header with SHA-256, and keep the sha256= prefix in mind.JSON.stringify(req.body) won't reproduce GitHub's bytes. Hash the raw body.application/x-www-form-urlencoded, the signed bytes are the whole form body (payload=...), not the decoded JSON. Switch the webhook to application/json, or hash the raw form body.app.post('/github', express.raw({ type: '*/*' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET.trim())
.update(req.body).digest('hex');
const got = req.get('X-Hub-Signature-256') || '';
const ok = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
res.sendStatus(202); // then process asynchronously
});
expected = 'sha256=' + hmac.new(secret.encode(), request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-Hub-Signature-256', '')):
abort(401)
The guide is Stripe-only. The kit is a self-hosted Cloudflare Worker relay that adds retries, replay and a dead-letter list in front of your endpoint. GitHub doesn't redeliver failed webhooks automatically, so retries matter here. It verifies X-Hub-Signature-256 out of the box. Set SCHEME_<ID>=github. It works the same way for Stripe, Lemon Squeezy, Shopify, Paddle Billing, Square and Twilio (status callbacks).
Fix Your Stripe Webhooks guide · Self-Hosted Webhook Relay Kit. Code LAUNCH40 = 40% off until Sun Oct 11, 6:40 AM MT.
GitHub expects a 2xx within 10 seconds on github.com, otherwise it marks the delivery "timed out". It doesn't redeliver failed deliveries automatically. Use Recent deliveries in the webhook settings (or the REST API) to redeliver. A redelivery keeps the same X-GitHub-Delivery ID, so de-duplicate on it.
Free download: Webhook debugging cheat sheet.
Fighting webhooks on more than one platform? The Fix Your Stripe Webhooks guide ($9) covers raw-body and signature problems in depth, and the Self-Hosted Webhook Relay Kit ($19) adds retries, replay and a dead-letter list in front of any endpoint.