Webhooks & signatures

Configure endpoints under Admin → Webhooks: a URL, a set of events, and a whsec_… signing secret shown at creation. Payloads deliberately carry ids and changed field names (plus coarse values like a status word) — fetch details through the read API, where token scopes apply.

Events

EventFires when
item.updatedAn item's fields changed.
item.movedAn item changed location.
service_entry.createdA service entry was logged.
loan.checked_outGear went out.
loan.checked_inGear came back.
qr.assignedA code was bound to an item.

Delivery

Every POST carries three headers: X-Leidang-Event, X-Leidang-Timestamp (unix seconds), and X-Leidang-Signature — sha256=<hex HMAC-SHA256(secret, timestamp.body)>. A non-2xx response is retried up to twice with backoff; each delivery's final outcome is inspectable under Admin → Webhooks.

Verifying the signature

Recompute the HMAC over the exact timestamp.rawBody string before parsing the body, compare constant-time, and reject stale timestamps to close replay windows:

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyLeidangWebhook(secret, req, rawBody) {
  const timestamp = req.headers["x-leidang-timestamp"];
  const header = req.headers["x-leidang-signature"]; // "sha256=<hex>"
  if (!timestamp || !header) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected =
    "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && timingSafeEqual(a, b);
}
Verify against the raw request bytes, not a re-serialized JSON object — any reformatting breaks the HMAC.