Home
Back to docs
Webhooks

How do I check that a webhook really came from this system?

Recompute the HMAC-SHA256 of the raw body with your secret and compare it with the X-Webhook-Signature header.

You will have made your receiving system reject any request that was not signed with your secret.

Before you start

You need the secret you entered for the endpoint, available to your receiving code, and access to the raw request body (the exact bytes, before any JSON parsing).

Steps

  1. Read the header X-Webhook-Signature. It looks like sha256=<hex digits>.
  2. Compute an HMAC with SHA-256 over the raw body, using your secret as the key, and write it in hex.
  3. Compare it with the part after sha256=, using a constant-time comparison.
  4. If they differ, answer with an error status and do nothing with the event.

For example, in Node:

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValid(rawBody, header, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const given = String(header).replace(/^sha256=/, '');
  return (
    given.length === expected.length &&
    timingSafeEqual(Buffer.from(given), Buffer.from(expected))
  );
}

What you will see

Genuine deliveries pass. Anything sent by someone who only knows your address fails.

If something goes wrong

  • Sign the raw body, not a re-serialised copy: a parser that reorders or reformats the JSON changes the signature.
  • The body contains ts, the time of sending, so it changes on every attempt. The signature covers the whole body, so each attempt has its own signature; that is normal.
  • Use X-Webhook-Delivery to ignore a repeat: it stays the same across retries, and an event can be delivered more than once.
  • X-Webhook-Event names the event. Respond with a 200-range status within 10 seconds, or the delivery is retried.

Last updated: