SDKs
Server SDK
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 install @lumavox/serverMake a client with a key from the console, and call:
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);Server only
Section titled “Server only”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.
The client
Section titled “The client”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.
const limits = await api.limits();const perAgent = limits.find((limit) => limit.key === "limits.agent.tools");console.log(`${perAgent?.label}: ${perAgent?.value}`);Errors
Section titled “Errors”Everything thrown is a PlatformError, with the API’s code to switch on — see Errors:
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.
Paging
Section titled “Paging”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.
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.
Waiting on jobs
Section titled “Waiting on jobs”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.
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.
Uploads
Section titled “Uploads”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.
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, anArrayBufferor a typed array; aBlobsays 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, thenconfirm(an asset),create(a voice) orfromFile(knowledge). See Assets.
Streams
Section titled “Streams”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.
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.
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.
End users and ephemeral keys
Section titled “End users and ephemeral keys”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.
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);