Skip to content
LogZuno

Verify webhook signatures

Check the LogZuno-Signature header with your signing secret before trusting a webhook, and reject replays.

A webhook destination sends alerts and reports to your own HTTPS endpoint as JSON. Every request carries a signature, so your endpoint can be sure it came from LogZuno and wasn't changed on the way.

The request

{
  "type": "event",
  "title": "Payment received",
  "body": "Growth plan, yearly",
  "url": "https://platform.logzuno.app/…",
  "project": "Web App",
  "fields": [],
  "data": {},
  "sent_at": "2026-10-23T10:00:00+00:00"
}

LogZuno waits up to 10 seconds for a 2xx answer. Any other answer, or no answer, is recorded as a failed delivery in the alert's history.

The signature

The LogZuno-Signature header looks like t=1792771200,v1=42d2…:

  • t is the Unix time the request was signed.
  • v1 is the hex HMAC-SHA256 of t, a dot and the raw request body, keyed with the destination's signing secret. Find it under Project settings → Destinations, on the webhook.

To verify, compute the same HMAC over the raw body (before any JSON parsing), compare it in constant time, and reject timestamps older than a few minutes so a captured request can't be replayed:

Verify a webhook (Node.js)
// Node.js: verify a LogZuno webhook before trusting it.
// LogZuno-Signature: t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyLogZunoSignature(rawBody, header, secret, toleranceSeconds = 300, now = Date.now() / 1000) {
  const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isInteger(t)) return false;
  // Reject old deliveries, so a captured request can't be replayed later.
  if (Math.abs(now - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express: read the raw body, verify, then parse.
// app.post("/hooks/logzuno", express.raw({ type: "application/json" }), (req, res) => {
//   const ok = verifyLogZunoSignature(req.body.toString("utf8"), req.get("LogZuno-Signature"), process.env.LOGZUNO_WEBHOOK_SECRET);
//   if (!ok) return res.sendStatus(401);
//   const notice = JSON.parse(req.body);
//   res.sendStatus(200);
// });

The same check works in any language with an HMAC-SHA256 function.