How do I verify a webhook signature?
Compute an HMAC-SHA256 of the raw request body — the exact bytes received, before any JSON parsing — with your signing secret, encode it the way the sender does, and compare it with the signature header in constant time. Almost every failure is one of three things: the body was parsed and re-serialized before hashing, the secret is the wrong one or carries a stray newline, or the encoding or prefix does not match the sender's.
The check
import { createHmac, timingSafeEqual } from 'node:crypto'
// Verify an HMAC-SHA256 webhook signature over the raw body.
// rawBody: the body exactly as received (Buffer or string), never JSON.stringify(req.body)
// encoding: 'hex' or 'base64' as the sender documents; prefix: for example 'sha256=' for GitHub
export function verifySignature ({ rawBody, secret, received, encoding = 'hex', prefix = '' }) {
if (typeof received !== 'string' || !received.startsWith(prefix)) return false
const expected = Buffer.from(createHmac('sha256', secret).update(rawBody).digest(encoding))
const given = Buffer.from(received.slice(prefix.length))
return given.length === expected.length && timingSafeEqual(given, expected)
}Each sender's scheme
| Sender | Header | Encoding | Prefix |
|---|---|---|---|
| GitHub | x-hub-signature-256 | hex | sha256= |
x-li-signature | hex | none | |
| QuickBooks | intuit-signature | base64 | none |
| Typeform | typeform-signature | base64 | sha256= |
Two common senders sign more than the body. Stripe signs a timestamp joined to the body and sends both in one header; Square signs your notification URL joined to the body. For where Square's key lives, see Where do I find my Square webhook signature key?
"No signatures found matching the expected signature for payload"
Stripe's library asks, in the same message, "Are you passing the raw request body you received from Stripe?" — so first make sure nothing like JSON.stringify(req.body) is passed in, then check the endpoint's signing secret for the wrong value or stray whitespace.
In Slashbin
Verification runs at the gateway from stored configuration — the header, the encoding and the prefix for each sender — before your Golden Model or any destination sees the event, so there is no verification code of yours to get wrong.
Need somewhere for the next project's webhooks to land? Slashbin's Free plan takes an account, not a card.
Start Free
(opens in a new tab)