Verifying signatures
Authenticate every delivery before acting on it. Each request includes a signature header you check against your endpoint's signing secret.
Header format
Every request includes an X-Klang-Signature header in the form t=<unix-timestamp>,v1=<hex>. v1 is HMAC-SHA256 of <t>.<raw-body>, keyed with your endpoint's signing secret.
Always verify before acting on a payload, and reject deliveries whose t is older than ~8 hours. This prevents replay attacks if a delivery is intercepted or leaked. The window is wide because we reuse the original timestamp across retries; pair it with id-based idempotency on your side.
Reference implementation
Drop this into your Express handler. Adapt the raw-body read to your framework if you're not using Express — the signature is computed over the bytes the request was delivered with, so reading the body as parsed JSON first will break verification.
import crypto from "node:crypto"; // Tolerance window: reject deliveries older than this (seconds). // Klang retries failed deliveries with backoff for up to ~7 hours, // reusing the original timestamp + signature. A few hours of slack is // safe; pair this with event_id idempotency on your side. const TOLERANCE_SECONDS = 28800; function verify(rawBody, header, secret) { const parts = Object.fromEntries( header.split(",").map((p) => p.split("=")) ); const t = Number(parts.t); const v1 = parts.v1; if (!t || !v1) throw new Error("malformed signature"); // Reject replays. if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) { throw new Error("timestamp out of tolerance"); } const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) { throw new Error("bad signature"); } return JSON.parse(rawBody); } app.post("/klang", express.raw({ type: "*/*" }), (req, res) => { try { const event = verify( req.body.toString(), req.headers["x-klang-signature"], process.env.KLANG_WEBHOOK_SECRET ); // safe to act on event.data } catch (err) { return res.status(401).send("invalid signature"); } res.status(200).end(); });