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