Voznest Forge

Workspace

WorkspaceProjects

Development

DocsAPI ReferenceSDKsEmbed StudioChangelog
  1. Forge
  2. /Docs

Start here

Getting StartedQuickstartPlatform ConceptsEnvironments

Build

ProjectsIdentityAgentsGatewayCapabilitiesPermissions ReferenceInstallationsLive activity

Commands & interactions

CommandsContext-menu CommandsInteractionsComponentsModals (Forms)Activities (Embedded Apps)

Acting in hubs

MessagesEmbedsEmbeds v2 (Blocks)Embeds v3Incoming WebhooksHub Management APIEvent StreamDelivery

Sign in with Voznest

AccessOpenID Connect

API

APIAuthenticationErrorsRate LimitsSecurity

Publish & operate

App Market & ListingTemplates & SandboxObservability & Team Tools

Tools

SDKsExamplesFAQ & TroubleshootingReference

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

HeaderMeaning
X-Quorvyn-EventEvent type, e.g. message.created
X-Quorvyn-DeliveryDelivery attempt id (stable across retries)
X-Quorvyn-TimestampUnix seconds when this attempt was signed
X-Quorvyn-Signaturev1=HMAC-SHA256(endpoint secret, timestamp + "." + body), hex
X-Quorvyn-Signature-Ed25519Ed25519 signature of the same string, base64url — verify with the project's public signing key
X-Quorvyn-Key-IdWhich 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.

ts
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)

ts
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)

ts
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.

PreviousEvent StreamNext Access