Verify webhook delivery
This guide shows the complete receiver pattern. See Webhooks for event and lifecycle concepts.
Express example
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
app.post(
"/assayra/events",
express.raw({ type: "application/json" }),
async (req, res) => {
const timestamp = String(req.header("Proofline-Timestamp") ?? "");
const signature = String(req.header("Proofline-Signature") ?? "");
const deliveryId = String(req.header("Proofline-Delivery-Id") ?? "");
const rawBody = Buffer.from(req.body);
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return res.sendStatus(401);
const expected = createHmac("sha256", process.env.ASSAYRA_WEBHOOK_SECRET!)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const supplied = Buffer.from(signature, "hex");
const computed = Buffer.from(expected, "hex");
if (
supplied.length !== computed.length ||
!timingSafeEqual(supplied, computed)
) {
return res.sendStatus(401);
}
const inserted = await events.insertOnce(deliveryId, rawBody);
res.sendStatus(202);
if (inserted) await queue.publish({ deliveryId });
},
);
Use the exact header names and signing envelope displayed by your Developer Hub because the endpoint contract is versioned.
Worker
async function handle(event: AssayraEvent) {
if (event.type !== "application.decided") return;
const application = await assayra.applications.get(
event.data.applicationId,
);
await customerAccounts.applyAssayraState(
application.application.externalReference,
application.application.status,
{ idempotencyKey: event.id },
);
}
Receiver checklist
- raw body captured before JSON middleware;
- timestamp tolerance enforced;
- constant-time signature comparison;
- delivery/event ID deduplicated in durable storage;
- fast
2xxacknowledgement; - asynchronous, retryable processing;
- application state reconciled from the API; and
- secret rotation and failure alerts tested.