Webhooks

Webhook Authentication

Every request Katexs sends is signed. Verify the signature before you trust a payload โ€” an unverified endpoint is an open door into your call data.


The signature header

Each delivery carries an x-katexs-signature header containing a timestamp and an HMAC-SHA256 hex digest of the raw request body, computed with your webhook signing secret.

text
x-katexs-signature: t=1785000461,v1=7f3a1c9e4b2d... 
x-katexs-event-id: evt_01J8XQ
x-katexs-delivery-attempt: 1

Get your signing secret

Go to Settings โ†’ Integrations โ†’ Webhooks and click Reveal signing secret. Store it as an environment variable on your server. Rotating it generates a new secret while the old one stays valid for 24 hours so you can deploy without dropping events.

Verify it

ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySignature(rawBody: string, header: string | null) {
  if (!header) return false;

  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=") as [string, string]),
  );
  const timestamp = Number(parts["t"]);
  const provided = parts["v1"];
  if (!timestamp || !provided) return false;

  // Reject anything older than five minutes to block replays.
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = createHmac("sha256", process.env["KATEXS_WEBHOOK_SECRET"]!)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(provided, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Rules that matter

  • Sign the raw body โ€” Verify before parsing. Re-serialized JSON produces a different digest and will never match.
  • Use a timing-safe compare โ€” A plain === leaks timing information and is a real attack surface.
  • Enforce the timestamp window โ€” Without it, a captured request can be replayed forever.
  • Deduplicate on event id โ€” Retries deliver the same x-katexs-event-id. Treat it as an idempotency key.
  • Fail closed โ€” Return 401 on a bad signature. Never process the payload anyway.

Additional layers

LayerWhen to use it
Static header secretYour API gateway needs a key before the request reaches your app.
mTLSRegulated environments where transport-level client identity is required.
IP allow-listDefense in depth. Request the current egress ranges from support; they change rarely but they do change.
Path entropyA hard-to-guess URL path reduces noise, but is not a substitute for signature verification.
If you suspect a secret has leaked, rotate it immediately in Settings โ†’ Integrations. Deliveries signed with the compromised secret stop being accepted after the 24-hour overlap.