Acting in hubs
Delivery
Signed HTTPS delivery with retries.
Endpoints receive events as POST requests with a JSON body. Production and Staging endpoints must be public https:// URLs; addresses that resolve to private or local networks are refused, at creation and on every attempt.
Headers
| Header | Meaning |
|---|---|
X-Quorvyn-Event | Event type, e.g. message.created |
X-Quorvyn-Delivery | Delivery attempt id (stable across retries) |
X-Quorvyn-Timestamp | Unix seconds when this attempt was signed |
X-Quorvyn-Signature | v1=HMAC-SHA256(endpoint secret, timestamp + "." + body), hex |
X-Quorvyn-Signature-Ed25519 | Ed25519 signature of the same string, base64url — verify with the project's public signing key |
X-Quorvyn-Key-Id | Which signing key signed it (rotation) |
Verifying with the SDK
verifyWebhook checks the headers, rejects timestamps more than 5 minutes away from now, compares the signature in constant time and refuses a signed request it already accepted (replay). Pass the raw body, exactly as received. Retries reuse the delivery id with a fresh timestamp and signature, so make your handler idempotent on event.id.
import express from "express";import { verifyWebhook } from "./voznest.mjs";const app = express();app.post("/voznest/events", express.raw({ type: "application/json", limit: "256kb" }), async (req, res) => { try { const { event } = await verifyWebhook({ headers: req.headers, body: req.body, secret: process.env.VOZNEST_DELIVERY_SECRET }); await handle(event); // idempotent on event.id res.sendStatus(204); } catch (e) { res.status(e.code === "replay" ? 200 : 401).end(); // never process an unverified body }});Verifying (HMAC)
import crypto from "node:crypto";import express from "express";const app = express();// Keep the raw body: the signature covers the exact bytes we sent.app.post("/voznest/events", express.raw({ type: "application/json" }), (req, res) => { const ts = req.get("X-Quorvyn-Timestamp"); const sig = req.get("X-Quorvyn-Signature") ?? ""; // "v1=<hex>" const signed = `${ts}.${req.body.toString("utf8")}`; const expected = "v1=" + crypto.createHmac("sha256", process.env.VOZNEST_DELIVERY_SECRET).update(signed).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; // reject replays older than 5 min const valid = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); if (!fresh || !valid) return res.status(401).end(); const event = JSON.parse(req.body.toString("utf8")); // Deliveries can repeat (retries): de-duplicate on event.id. console.log(event.type, event.hubId, event.data); res.status(204).end(); // any 2xx within 10 s = delivered});Verifying (Ed25519)
import crypto from "node:crypto";// Public key from Secrets → Signing keys (base64url, raw 32-byte Ed25519 key).const publicKey = crypto.createPublicKey({ key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(process.env.VOZNEST_SIGNING_PUBLIC_KEY, "base64url")]), format: "der", type: "spki",});export function verifyEd25519(rawBody, headers) { const signed = Buffer.from(`${headers["x-quorvyn-timestamp"]}.${rawBody}`); return crypto.verify(null, signed, publicKey, Buffer.from(headers["x-quorvyn-signature-ed25519"], "base64url"));}Retries
Any 2xx within 10 seconds counts as delivered. Otherwise the attempt is retried after 30 s, 2 min, 10 min, 30 min, 2 h, 6 h, 12 h and 24 h, up to the endpoint's retry limit (default 5, max 8). After 25 consecutive failures the endpoint is paused and the pause is recorded in Audit. Failed attempts can be retried by hand from the portal.