Oferta de lançamento: −50 % no seu primeiro pagamento, até 30 de novembro.Ver a oferta →
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.

Perguntas frequentes

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

Oferta de lançamento · −50 %

Depurando uma integração de webhook esta semana?

O Pass 7 dias desbloqueia as explicações com IA e o código do handler pronto para colar, 25 URLs permanentes, 30 dias de histórico e o túnel Relay para localhost. Um único pagamento, sem assinatura.

€ 5 € 2,50 uma vez
Pagamento único · sem assinatura · reembolso em 14 dias · preço de lançamento até 30 de novembro
GitHub webhook X-Hub-Signature-256 mismatch — how to verify it correctly