Article · Updated November 1, 2026 · By the Hooks team
How to verify a GitHub webhook signature
Short answer: GitHub signs the raw request body with HMAC-SHA256 using the webhook secret and sends the result in X-Hub-Signature-256 as sha256=<hex>. Compute the same digest over the exact bytes you received, compare it in constant time, and reject the request when they differ. Never verify against a re-serialized JSON object.
Most failed verifications come from one mistake: parsing JSON first and signing the re-encoded result. Key order, whitespace, and unicode escaping change the bytes, so the digest no longer matches.
Verify in Node.js
Read the body as bytes, hash those bytes, then compare. The comparison must take the same time whether the first byte or the last byte differs.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = "sha256=" + createHmac("sha256", process.env.GITHUB_SECRET)
.update(rawBody)
.digest("hex");
const received = request.headers.get("x-hub-signature-256") ?? "";
const ok = received.length === expected.length &&
timingSafeEqual(Buffer.from(received), Buffer.from(expected));Deduplicate with the delivery id
GitHub retries deliveries and can send the same event twice. X-GitHub-Delivery is stable across those retries, so store it and drop duplicates before running side effects.
Let the gateway do it
Hooks verifies the signature on arrival using the endpoint secret, rejects stale timestamps where the provider supplies one, and records the delivery id for dedupe. Your handler receives only verified events, and the body it gets is the one that was verified.
Frequently asked questions
Why does my signature check pass in tests but fail in production?
Tests usually sign the same string they verify. In production, a framework may parse and re-encode the body before your handler runs. Capture the raw bytes at the edge and sign those.
Does GitHub include a timestamp for replay protection?
No. GitHub's signature covers the body only, so replay protection comes from deduplicating X-GitHub-Delivery rather than rejecting old timestamps.