API Reference
Webhook signatures
Verify that a delivery actually came from Ceevee.
Verifying signatures is mandatory in production. Without verification, anyone who learns your endpoint URL can post forged events.
Header format
Ceevee sends a comma-separated header: a t= timestamp, then one or more v1= HMAC-SHA-256 signatures. During the 24h window after rotating a secret you may receive two v1= values — accept the request if either matches.
Ceevee-Signature: t=1714377600,v1=abcdef…1234,v1=fedcba…9876Verification recipe
- Read the raw request body — never the parsed JSON. Whitespace matters for the HMAC.
- Build the signed string:
${t}.${rawBody}. - Compute
hmac_sha256(secret, signedString)in hex. - Compare to each
v1=value with a timing-safe comparison (crypto.timingSafeEqualin Node). - Reject the request if
Math.abs(now - t) > 300seconds.
Node.js example
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(
rawBody: string,
header: string,
secret: string,
) {
const parts = Object.fromEntries(
header.split(",").map((p) => {
const [k, v] = p.split("=");
return [k.trim(), v.trim()];
}),
);
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) {
throw new Error("Stale signature");
}
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const provided = (parts.v1 ?? "");
if (
expected.length !== provided.length ||
!timingSafeEqual(Buffer.from(expected), Buffer.from(provided))
) {
throw new Error("Bad signature");
}
}