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…9876

Verification 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.timingSafeEqual in Node).
  • Reject the request if Math.abs(now - t) > 300 seconds.

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");
  }
}