Signed events
One signed stream for every provider. Each delivery is a POST with DCP-Event-Id, DCP-Event-Type and DCP-Signature:
DCP-Signature: t=1792000000,v1=5f2c…a9e1
v1 is the hex HMAC-SHA256 of <t>.<raw body> with your endpoint's signing secret. While a secret rotation overlaps, the header carries one v1 per active secret. Verify against the raw bytes, compare in constant time and reject a timestamp more than 5 minutes from now.
Verify
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300; // reject events signed more than 5 minutes ago
// rawBody: the exact bytes received (Buffer or string), before JSON.parse.
// header: the DCP-Signature request header.
export function verifyDcpSignature(rawBody, header, secret, now = Date.now()) {
const parts = String(header ?? "").split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!Number.isInteger(t) || Math.abs(now / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
// During a secret rotation the header carries one v1 per active secret.
return parts
.filter(([k]) => k === "v1")
.some(([, v]) => {
const given = Buffer.from(v ?? "", "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}Answer with any 2xx within 10 seconds. Otherwise the delivery is retried at 1m, 5m, 15m, 30m, 1h, 2h, 3h, 4h, 6h and 7h after the previous attempt (about 24 hours), then dead-lettered; you can replay it from the dashboard with the same DCP-Event-Id. Deliveries can arrive late or twice: deduplicate on DCP-Event-Id.