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
- Read the header
X-Webhook-Signature. It looks likesha256=<hex digits>. - Compute an HMAC with SHA-256 over the raw body, using your secret as the key, and write it in hex.
- Compare it with the part after
sha256=, using a constant-time comparison. - 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-Deliveryto ignore a repeat: it stays the same across retries, and an event can be delivered more than once. X-Webhook-Eventnames the event. Respond with a 200-range status within 10 seconds, or the delivery is retried.