TypeScript
The official client for TypeScript and JavaScript.
The vojo package is the official client for TypeScript and JavaScript. It needs Node.js 20 or later and has no runtime dependencies.
Install
The package isn't published yet. Until it is, ask your Vojo contact for a copy.
Make your first call
import Vojo from "vojo";
const vojo = new Vojo(process.env.VOJO_KEY);
const organisation = await vojo.organisation.retrieve();
console.log(organisation.name);Leave the key out and the client reads VOJO_SECRET_KEY. Keep your key on your server: the client refuses to start in a browser, because anyone can read a key from a page.
Call the API
Methods follow the API's paths, and field names match the API exactly, in snake_case. Each reference page shows its method. Most methods return the resource itself. Statistics and issues keep data and meta.
const site = await vojo.sites.create({ name: "Leeds Trinity" });
const screen = await vojo.screens.create({ name: "Lobby North", site_id: site.id, size_inches: 55 });
const same = await vojo.screens.retrieve(screen.id);
await vojo.screens.update(screen.id, { name: "Lobby South" });Page through a list
for await walks every item across every page, fetching more as it goes.
for await (const screen of vojo.screens.list({ has_harvester: true })) {
console.log(screen.name);
}await gives you one page, if you want to page yourself.
const page = await vojo.screens.list({ limit: 100 });
console.log(page.data.length, page.meta.has_more, page.meta.next_cursor);To collect into an array, say how many you're willing to hold: await vojo.screens.list().toArray({ limit: 5000 }).
Handle errors
import { VojoConflictError, VojoValidationError } from "vojo";
try {
await vojo.screens.create({ name: "Lobby North", size_inches: 55 });
} catch (error) {
if (error instanceof VojoValidationError) {
for (const field of error.fieldErrors) console.error(field.pointer, field.detail);
} else if (error instanceof VojoConflictError && error.code === "duplicate_name") {
console.error("That name is taken.");
} else {
throw error;
}
}| Class | When |
|---|---|
VojoAuthenticationError |
401: the key is missing, invalid, expired or revoked |
VojoPermissionError |
402 or 403: a missing scope or permission, or a plan limit |
VojoNotFoundError |
404: not found, or it belongs to another organisation |
VojoValidationError |
400, 413 or 422: see fieldErrors |
VojoConflictError |
409 |
VojoRateLimitError |
429: see retryAfter |
VojoServerError |
5xx |
VojoConnectionError |
No response. VojoTimeoutError is the timeout case |
VojoPaginationError |
The API's paging data couldn't be followed safely |
API errors all extend VojoAPIError, with status, code, problem (the full problem response) and requestId. Branch on code, not on message text, and always handle a code you haven't seen, because new ones can be added.
Verify webhooks
Verifying needs only the endpoint's signing secret, not an API key. Pass the raw request body, because the signature covers the exact bytes Vojo sent.
import express from "express";
import { webhooks, VojoWebhookVerificationError } from "vojo";
const app = express();
app.post("/vojo", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhooks.constructEvent(req.body, req.get("X-Vojo-Signature"), process.env.VOJO_WEBHOOK_SECRET!);
} catch (error) {
if (error instanceof VojoWebhookVerificationError) return res.status(400).end();
throw error;
}
res.status(204).end(); // Acknowledge first, then do the work.
if (event.type === "verification.score.final") {
console.log(`Play ${event.payload.playId} scored`);
}
});Deliveries can repeat, so deduplicate on event.id, which stays the same across retries. While you switch to a new secret after a roll, pass both: constructEvent(body, header, [newSecret, oldSecret]).
To test your own handler, sign a payload with webhooks.generateTestHeader({ payload, secret }).
Retries and idempotency
Failed requests are retried twice, with backoff, when that's safe. Rate limits and connections that never opened are retried on every request, unless Retry-After is more than 60 seconds, which comes back to you as the error. GET, PUT, DELETE and creates are also retried after 5xx errors and dropped connections. PATCH and other POSTs aren't, because the first attempt may have worked.
Every create is sent with an Idempotency-Key, reused across its retries. To retry across your own restarts, supply your own.
await vojo.sites.create(body, { idempotencyKey: `site-${yourRecord.id}` });Configure the client
const vojo = new Vojo(key, {
timeout: 30_000, // Per attempt, in ms. Default 60000.
maxRetries: 5, // Default 2.
version: "2026-10-07", // Default: the version this SDK was generated against.
organisation: "org_id", // OAuth access tokens only.
baseUrl: "http://127.0.0.1:4020", // For example, the mock server.
});
// Most options work per request too.
await vojo.screens.retrieve(id, { timeout: 5_000, maxRetries: 0 });Every result also has lastResponse, with status, headers, requestId, apiVersion, idempotentReplayed and meta. Check wrapper.lastResponse.meta.warnings after you wrap a creative, and quote requestId when you contact Vojo.