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

Tools

Examples

Complete, copyable programs.

A poll agent (SDK)

ts
import { botFromEnv, embed, button, row, loadEnv } from "./voznest.mjs";await loadEnv(".env"); // VOZNEST_API_URL, VOZNEST_BOT_TOKEN, VOZNEST_APP_IDconst bot = botFromEnv();bot.command({ name: "ping", description: "Check the agent is alive" }, async (ctx) => {  await ctx.reply({ content: `Pong — ${await bot.ping()} ms` });});bot.command(  { name: "poll", description: "Ask a quick question", options: [{ type: "string", name: "question", description: "What to ask", required: true }] },  (ctx) => ctx.reply({    embeds: [embed().title(ctx.option("question")).color("violet")],    components: [row(button({ label: "Yes", customId: "poll:yes", style: "success" }), button({ label: "No", customId: "poll:no", style: "danger" }))],  }),);bot.button("poll:", (ctx, choice) => ctx.reply({ content: `You voted ${choice}`, ephemeral: true }));await bot.start({ activity: { type: "playing", name: "/poll" } });

Receive events on your server

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});

“Sign in with Voznest”

ts
// 1. Send the person to the consent screen (PKCE, S256)const verifier = base64url(crypto.randomBytes(32));const challenge = base64url(crypto.createHash("sha256").update(verifier).digest());const url = new URL("https://voznest.com/oauth/authorize");url.search = new URLSearchParams({  client_id: "<project id>",            // "<project id>.development" for Development  redirect_uri: "https://example.com/callback",  // registered in Access, exact match  response_type: "code",  scope: "identify email",  state: csrfToken,  code_challenge: challenge,  code_challenge_method: "S256",}).toString();
ts
// 2. On your callback: exchange the code (valid 5 minutes, single use)const res = await fetch("https://api.voznest.com/api/v1/oauth/token", {  method: "POST",  headers: { "Content-Type": "application/json" },  body: JSON.stringify({    grant_type: "authorization_code",    code, redirect_uri: "https://example.com/callback",    client_id: "<project id>",    client_secret: process.env.VOZNEST_CLIENT_SECRET, // or code_verifier for public clients    code_verifier: verifier,  }),});const { access_token, refresh_token, expires_in, scope } = await res.json();// 3. Call the API as that personconst me = await fetch("https://api.voznest.com/api/v1/oauth/@me", { headers: { Authorization: `Bearer ${access_token}` } }).then((r) => r.json());

Publish commands from CI

bash
# Publish the command list from CI with an API key (scope: commands.write)curl -X PUT "https://api.voznest.com/api/v1/projects/@self/commands" \  -H "Authorization: Key $VOZNEST_API_KEY" \  -H "Content-Type: application/json" \  --data @commands.json
PreviousSDKsNext FAQ & Troubleshooting