Docs
open_in_new Sign in Get API key
Webhooks

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