Launch offer: −50 % on your first payment, until November 30.See the offer →
webhook·toolkit

GitHub webhook signature mismatch (X-Hub-Signature-256)

GitHub signs deliveries when the webhook has a secret. The X-Hub-Signature-256 header holds sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with that secret. A mismatch almost always comes from verifying something other than the raw body.

Open the GitHub signature validator →

The check, done right

Compute the HMAC over the bytes you received, prefix it with sha256=, and compare with a constant-time function. Do not compare with ===: it leaks timing information.

Node.js
import crypto from 'node:crypto';

function verifyGithub(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signature || '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hmac, hashlib

def verify_github(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or '')

Cause 1: the body was parsed, then re-serialised

Same trap as with every provider: if a JSON middleware ran first, JSON.stringify(req.body) is not what GitHub sent. Read the raw body on the webhook route (express.raw(), await req.text(), request.body in Django).

Cause 2: the content type is application/x-www-form-urlencoded

A GitHub webhook can be set to send application/x-www-form-urlencoded. The body is then payload= followed by the URL-encoded JSON, and the signature covers that whole form body, not the decoded JSON. Either switch the webhook to application/json in the repository settings, or verify the raw form body before decoding it.

Cause 3: the old SHA-1 header or the wrong secret

X-Hub-Signature (without -256) is the legacy SHA-1 signature. Compare X-Hub-Signature-256 with a SHA-256 HMAC. The secret is the one typed in the webhook settings (Settings → Webhooks → Edit); a GitHub App has its own webhook secret in the app settings.

Frequently asked questions

Does GitHub sign the request if I leave the secret empty?

No. Without a secret there is no signature header to verify. Set a secret and store it in an environment variable.

How can I see the exact body GitHub sent?

Point the webhook at a Webhook Toolkit URL: the inspector keeps the raw body and headers, and Recent Deliveries in GitHub lets you redeliver the same event.

Other webhook guides

Launch offer · −50 %

Debugging a webhook integration this week?

The 7-day Pass unlocks AI explanations and ready-to-paste handler code, 25 permanent URLs, 30-day history and the Relay tunnel to localhost. Pay once, no subscription.

€5 €2.50 once
One-time payment · no subscription · 14-day money-back · launch price until November 30
GitHub webhook X-Hub-Signature-256 mismatch — how to verify it correctly