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.
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);
}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.