Coinfat

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

  1. Read the raw request body before JSON-parsing (re-serializing changes the bytes and breaks the check).
  2. Parse X-Signature into t and v1.
  3. Recompute the expected HMAC-SHA256 of the timestamp + dot + raw body, as hex.
  4. Compare it to v1 with a constant-time comparison.
  5. 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 || "")
);
}