Skip to content
Console

SDKs

Server SDK

Copy page

The TypeScript client for your backend — install, errors, paging, waiting on jobs, uploads and streams, with your organisation's API key.

@lumavox/server is the API from your server, with your organisation’s API key: every operation a key may call, as one method each, typed from the API reference. It has no dependencies — fetch and the web platform only — and runs on Node 20.3 or later, Cloudflare Workers, Deno and Bun.

npm
npm install @lumavox/server

Make a client with a key from the console, and call:

TypeScript
import { createClient } from "@lumavox/server";
const api = createClient({
apiKey: process.env.API_KEY!,
baseUrl: "https://api.lumavox.ai",
});
const image = await api.images.generate({ prompt: "A paper boat on a rain-soaked street" });
console.log(image.status, image.image?.url);

The client holds your API key, which spends your whole balance and reads every end user, so it refuses to run in a browser or a React Native app: bundlers that build for a browser resolve it to a module that fails the build and throws when it loads, and createClient checks again. There is no way round it. A device talks to agents with an ephemeral key of its own, minted on your server with ephemeralKeys.create.

TypeScript
createClient({
apiKey, // an org API key; nothing is read from the environment
baseUrl: "https://api.lumavox.ai",
timeoutMs: 30_000, // per attempt
maxRetries: 2,
fetch, // the runtime's own by default
});

Bodies and answers are the API’s own JSON, field for field and in its snake_case, as the reference shows them; methods are camelCase. Credits are integers of micro-credits (credit_limit_micros: 5_000_000 is 5 credits) and times ISO strings, as on the wire — see Conventions. Every schema is exported as a type — GenerationInfo, AssetDetail, EndUserInfo and the rest, and Page is any list’s page, { data, next_cursor }.

Every method takes the SDK’s own options last: { timeoutMs, signal, maxRetries, requestId }. signal aborts the call, its retries and its waits, and is thrown as its own reason.

Resource Methods
endUsers create, get, update, delete, list, page, export; facts.list, facts.add, facts.update, facts.delete
ephemeralKeys limits, create, get, revoke, inspect, list, page
images, videos, audio, skybox, transcriptions create, get, wait, generate; skybox.styles
generations get, wait, list, page
templates list, page, get, create, generate, file
tts, stt create
voices list, page, get, create, uploadUrl, upload, update, delete, resample, design, saveDesign, wait
assets list, page, get, uploadUrl, confirm, upload, update, delete, deleteMany, move, wait; assets.folders: list, page, get, create, update, delete
agents list, page, get, create, update, delete; agents.settings: get, update; agents.knowledge, agents.tools: list, set
tools list, page, get, create, update, delete, rotateSecret, test, calls
knowledge list, page, get, create, bulk, fromUrl, uploadUrl, fromFile, upload, update, delete, reindex, refresh, setAgents, search, categories, insights, wait
usage get, plan
the client health() and limits(), which send no key

A delete of something already gone is done, not an error. A delete of one thing answers true, or false when there was nothing by that id; one that reports what went with it — an end user’s sessions and facts, the assets a folder moved up — answers those counts, or null when there was nothing. Neither throws a NotFoundError.

limits() reads the platform’s limits as they stand now — the tools an agent may have, the size of an upload, the ceilings on an ephemeral key — each by the key the reference names it by. The platform’s staff may change them, so read one rather than writing its number down. See Limits.

TypeScript
const limits = await api.limits();
const perAgent = limits.find((limit) => limit.key === "limits.agent.tools");
console.log(`${perAgent?.label}: ${perAgent?.value}`);

Everything thrown is a PlatformError, with the API’s code to switch on — see Errors:

TypeScript
import { InsufficientCreditsError, PlatformError, RateLimitError } from "@lumavox/server";
try {
await api.ephemeralKeys.create({ end_user_id: "user_8842", credit_limit_micros: 5_000_000 });
} catch (error) {
if (error instanceof InsufficientCreditsError) {
// top up, or mint a smaller key
} else if (error instanceof PlatformError) {
console.error(error.status, error.code, error.message, error.requestId);
}
throw error;
}
Class When
ValidationError 400, 422; issues says which fields
AuthenticationError 401
InsufficientCreditsError 402
PermissionError 403: a scope the key lacks
NotFoundError 404
ConflictError 409: a plan’s maximum, an id already taken, or a change since it was read
RateLimitError 429; retryAfter is in seconds
ServerError 5xx
ConnectionError no response; cause is what fetch threw
TimeoutError no response within timeoutMs, on the last attempt

code is the API’s — validation_failed, insufficient_credits, rate_limited, … — or, when no answer of the API’s came back, the SDK’s own: connection_error, timeout, cloudflare_error (refused before it reached the API), http_error (an error body that was not the API’s) or invalid_response (a success that was not JSON).

Quote requestId when you report a problem: it is the X-Request-Id every attempt of the call carried.

Only what cannot happen twice is retried, up to maxRetries: a GET, PUT, PATCH or DELETE after a failed connection, a timeout, 429, 502, 503 or 504; a POST after a 429 only, which is refused before any work is done — and endUsers.create with an external_id, which is idempotent. The wait doubles from 0.5 s to 8 s with jitter; Retry-After wins, up to a minute, and a longer one is thrown at once as a RateLimitError.

list() is an async iterator over every item, reading the next page as you reach it; page() is one page, as the route answers it: { data, next_cursor }. Stop early and nothing more is read.

TypeScript
for await (const asset of api.assets.list({ media_type: "audio" })) {
console.log(asset.id, asset.name);
}
const first = await api.endUsers.page({ limit: 50 });
if (first.next_cursor) {
const second = await api.endUsers.page({ limit: 50, cursor: first.next_cursor });
console.log(second.data.length);
}

Every list pages the same way — by the next_cursor each page names, null on the last — so nothing is seen twice, and no list counts its rows. list() reads the most a route allows per page unless you set limit. Voices, agents, templates and a folder’s level answer everything in one page; their list() reads that page, and would read on the day the route pages. skybox.styles() and knowledge.categories() answer their one page as the route does.

generate(body) creates a queued job and waits for it to finish; wait(id) waits for one you made earlier. Both answer the finished job — succeeded, failed or moderated — and never throw for a failure: read status as you would from the API. A failed or moderated job is refunded.

TypeScript
const job = await api.videos.generate(
{ prompt: "Waves breaking at dusk", duration: 5 },
{ signal: AbortSignal.timeout(10 * 60_000) },
);
if (job.status !== "succeeded") throw new Error(job.error ?? job.status);

The same goes for voices, assets and knowledge entries, which are pending until they are ready or failed. The SDK reads the job again after 1 s, then each time half as long again, at most 10 s apart. A wait has no deadline of its own: pass a signal, or the job’s own deadline on the platform ends it. Polls are not counted against your rate limits.

templates.create(id, body) is the template’s own submit, answering at once; templates.generate(id, body) waits, as images.generate does.

upload() is the whole upload in one call: it asks for a signed URL, sends the bytes there — straight to object storage, never through the API — and tells the API they arrived.

TypeScript
import { readFile } from "node:fs/promises";
const file = await readFile("greeting.wav");
const asset = await api.assets.upload(
file,
{ content_type: "audio/wav", name: "Greeting" },
{ wait: true },
);
const voice = await api.voices.upload(recording, { name: "Narrator", consent: true });
const entry = await api.knowledge.upload(pdf, { filename: "returns.pdf" });
  • A Blob, an ArrayBuffer or a typed array; a Blob says its own type. A stream only with its size — { size } in the last argument — since a signed upload needs its length, and a stream is sent once, never retried.
  • { wait: true } answers once processing is over — the asset converted, the voice cloned, the file read and indexed — instead of at once.
  • The steps are there on their own too: uploadUrl, then confirm (an asset), create (a voice) or fromFile (knowledge). See Assets.

Speech, streamed: tts.create with stream: true answers as soon as the first audio is made, with the body as a ReadableStream of raw PCM — 16-bit, mono, at sampleRate. Without stream it answers the whole file as an ArrayBuffer. Both carry what the response’s headers say. See Text to speech.

TypeScript
const speech = await api.tts.create({ text: "Hello there", format: "pcm", stream: true });
console.log(speech.sampleRate, speech.model);
for await (const chunk of speech.body) {
player.write(chunk);
}

A transcript, streamed: stt.create(audio, { stream: true }) is an async iterator of the transcript’s events as they arrive — a segment as each piece is cut, its transcript (or error) in order, and a done with the whole text. WAV or raw PCM only; without stream, any audio, answered whole. Up to 25 MB — for longer audio, upload it and use transcriptions. See Transcription.

TypeScript
for await (const event of api.stt.create(wav, { stream: true })) {
if (event.type === "transcript") console.log(event.index, event.text);
if (event.type === "done") console.log("whole:", event.text);
}

A stream’s timeout ends at its headers: a transcript read for minutes is not a call that timed out. Your signal still ends it, and leaving the loop early hangs up.

endUsers.create({ external_id }) makes a person once and finds them every time after — created says which. ephemeralKeys.create mints their device a key with a credit limit set aside from your balance, clamping its lifetime to the tighter of the platform’s maximum and your plan’s — the API itself never clamps: without the SDK, or with { clampTtl: false }, a lifetime past a cap is refused with 422. ephemeralKeys.revoke(id, { settled: true }) answers once the key’s final figures are in, its settled_at set; without it, at once. endUsers.export(id) reads every conversation and fact the platform holds about them — replaced facts too, until they are deleted — with a five-minute key of their own. endUsers.facts.add(id, { text }) tells the agents what your records hold — created says whether it was new — and facts.update, facts.list and facts.delete correct, read and forget, with no key minted. See End users.

tools.create adds a tool to your organisation’s library — your server, called with a signed POST, or the person’s app — and answers a webhook’s signing secret once, as secret; rotateSecret answers a new one. agents.tools.set turns tools on for an agent, up to the platform’s limit (see Limits), and test sends your server one call exactly as a conversation would. calls reads a tool’s most recent calls in your agents’ conversations, and each tool carries recent_failures. See Tools.

TypeScript
const tool = await api.tools.create({
name: "lookup_order",
description: "Look an order up by the number on the customer's receipt.",
kind: "webhook",
url: "https://shop.example.com/tools/lookup-order",
parameters: {
type: "object",
properties: { order_number: { type: "string" } },
required: ["order_number"],
},
headers: { Authorization: `Bearer ${process.env.SHOP_TOKEN}` },
});
// `tool.secret` is shown this once: keep it with your server.
await api.agents.tools.set("0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07", {
tool_ids: [tool.id],
});
const answer = await api.tools.test(tool.id, {
arguments: { order_number: "A-1042" },
});
console.log(answer.ok, answer.status, answer.result);
const { data } = await api.tools.calls(tool.id, { limit: 20 });
for (const call of data) console.log(call.at, call.ok, call.duration_ms, call.error);