Skip to main content
webhooks

HMAC: how webhooks know you are not lying

Verify a webhook HMAC over the untouched request bytes with a constant-time comparison before parsing or acting on the payload.

Thien Nguyen
By Thien Nguyen
Updated April 7, 2026 · 1 min read

A webhook HMAC proves that someone holding a shared secret signed a particular byte sequence. It does not encrypt the payload, and it does not automatically prevent replay. Verify it before business logic, then add timestamp and event-id checks if the provider supports them.

The order matters

raw request body → HMAC(secret, raw bytes) → constant-time compare → parse JSON → process event

Parsing and reserialising JSON before verification is a common bug. Whitespace, key ordering, and Unicode representation can change even when the object looks identical.

const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const actual = Buffer.from(signature, 'hex');
const expectedBytes = Buffer.from(expected, 'hex');
if (actual.length !== expectedBytes.length || !crypto.timingSafeEqual(expectedBytes, actual)) {
  throw new Error('Invalid webhook signature');
}
RequirementWhy
Raw bytesMatches exactly what the sender signed
Constant-time comparisonAvoids leaking partial-match timing
Keyed secretStops anyone who only knows the algorithm
Event id / timestampLimits duplicate or delayed delivery

A valid signature says the sender is authentic; it does not say this is the first time you have received the event. Make handlers idempotent.

Return a 2xx only after durable acceptance, such as enqueueing the event or recording its id. Providers retry on timeouts and many retry on 5xx, so a slow handler should hand off work rather than make the signature endpoint do the entire job.

References

Primary documentation and specifications checked when this article was last updated.

webhookssecurityapi

Related articles

All articles