With .reason:
"bad_timestamp" — timestamp header isn't a finite number."stale_timestamp" — timestamp outside tolerance (replay attempt?)."missing_prefix" — signature doesn't start with sha256=."length_mismatch" — hex digest has wrong length."mismatch" — HMAC didn't match (tampered payload / wrong secret)."invalid_payload_type" — payload wasn't a Buffer / Uint8Array.import express from "express";
import { verifyWebhookSignature, NessyWebhookSignatureError } from "@nessyapi/sdk/webhook";
app.post(
"/webhooks/nessy",
express.raw({ type: "application/json", limit: "1mb" }),
(req, res) => {
try {
verifyWebhookSignature({
payload: req.body,
secret: process.env.NESSY_WEBHOOK_SECRET!,
signatureHeader: req.header("x-nessyapi-signature-256") ?? "",
timestampHeader: req.header("x-nessyapi-timestamp") ?? "",
});
} catch (err) {
if (err instanceof NessyWebhookSignatureError) return res.status(401).end();
throw err;
}
const event = JSON.parse(req.body.toString("utf8"));
// handle event.event, event.data ...
res.status(200).end();
}
);
export async function POST(req: Request) {
const payload = Buffer.from(await req.arrayBuffer());
verifyWebhookSignature({
payload,
secret: process.env.NESSY_WEBHOOK_SECRET!,
signatureHeader: req.headers.get("x-nessyapi-signature-256") ?? "",
timestampHeader: req.headers.get("x-nessyapi-timestamp") ?? "",
});
// handle the parsed event
return new Response(null, { status: 200 });
}
Verify a NessyAPI webhook signature. Throws NessyWebhookSignatureError on any failure — never returns
false, so you can't accidentally skip a branch.The signature is HMAC-SHA256 over
payload + "." + timestamp_bytes, hex- encoded, prefixed withsha256=. Timestamps prevent replay attacks within thetoleranceSecondswindow (default 5 minutes).Security notes:
crypto.timingSafeEqualon equal-length Buffers (no byte-leakage via early-exit comparison).if (!valid)patterns are easy to miss.