Webhooks
Verifying signatures
Every delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Verify it before trusting any webhook.
The signature header
bash
X-Signature: t=<unix_timestamp>,v1=<hex_hmac_sha256>
v1 is the lowercase hex HMAC-SHA256 of the timestamp, a literal dot, then the exact raw request body, keyed by your signing secret.
bash
X-Signature: t=1751899930,v1=6f2e...c1a9
How to verify
- Read the raw request body before JSON-parsing (re-serializing changes the bytes and breaks the check).
- Parse
X-Signatureintotandv1. - Recompute the expected HMAC-SHA256 of the timestamp + dot + raw body, as hex.
- Compare it to v1 with a constant-time comparison.
- Reject if the timestamp is outside your tolerance window (e.g. ±5 minutes) to prevent replay.
Verification code
const crypto = require("crypto");function verify(rawBody, header, secret, toleranceSec = 300) {const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));const t = Number(parts.t);if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");return crypto.timingSafeEqual(Buffer.from(expected),Buffer.from(parts.v1 || ""));}
Always use the raw request body: many frameworks parse JSON before your handler runs, which changes the bytes and fails the check.