Skip to content

Verifying signatures

Every delivery is signed with your endpoint’s secret. Verify the signature before you act on an event; anyone who knows your URL could otherwise post fake events.

Two headers carry what you need:

  • X-Webhook-Timestamp: when the attempt was made, in seconds since the Unix epoch. It is renewed on every retry.
  • X-Webhook-Signature: v1= followed by the hex HMAC-SHA256 of "<X-Webhook-Timestamp>.<raw request body>", keyed with the endpoint’s whsec_... secret.

The SDKs verify the signature and the timestamp and return the parsed event.

import { constructWebhookEvent, WebhookSignatureError } from '@clearance-rest/sdk';
// Express: read the body as text on this route, for example express.text({ type: 'application/json' }).
app.post('/hooks/clearance', async (req, res) => {
try {
const event = await constructWebhookEvent(req.body, req.headers, process.env.WEBHOOK_SECRET!); // 300 s tolerance
if (event.type === 'number.renewal_failed') {
console.log(event.data.reason);
}
res.sendStatus(200);
} catch (error) {
if (error instanceof WebhookSignatureError) return res.sendStatus(400);
throw error;
}
});

constructWebhookEvent takes the raw body as a string, and the headers as a Headers object or a plain object. verifyWebhookSignature(raw, headers, secret, { toleranceSec }) returns a boolean instead of throwing. Both use Web Crypto and run on Node, Cloudflare Workers and in browsers. They are also available as client.webhooks.constructEvent and client.webhooks.verifySignature.

Respond with a 2xx as soon as the signature checks out, and handle the event afterwards. Deliveries are at least once, so deduplicate on event.id.

POST /webhooks/endpoints/{id}/rotate-secret returns a new secret and invalidates the old one at once, so deliveries fail verification until your receiver uses the new secret. Update your receiver right after rotating; failed deliveries are retried for about a day, and you can retry any that ran out.