JavaScript SDK / Management API
Management API
Everything that is not the live conversation: minting the tokens a browser streams with, creating avatars and voices, reading and writing configuration. It is a namespace on the client rather than more methods, so the privileged half is visible in one place instead of scattered through the surface a page uses.
ManagementApi takes a long-lived API key
(zsk_live_). Anything in page JavaScript is readable by every
visitor, and a leaked full key can create avatars, upload voices, change
settings, delete things and mint more tokens. The browser gets a
short-lived session token (zsk_temp_) and
nothing else. If you find yourself importing this into client code, the fix is
a server route, not a smaller key.
The boundary, in one picture
| Runs where | Credential | Can do | |
|---|---|---|---|
ManagementApi | Your server | zsk_live_ API key | Everything on this page |
ZeliClient session half | The browser | zsk_temp_ session token | Stream, talk, interrupt |
The token is stream only, expires in minutes, and the box refuses every mutating route for it, so a leaked one cannot do damage. That property only holds while the key that minted it stays on your server.
import { ZeliClient, type ManagementApi } from "@zeligate/zeli-avatar";
// your backend, never bundled for the browser
const admin = new ZeliClient({
serverUrl: process.env.ZELI_BOX_URL!,
apiKey: process.env.ZELI_API_KEY!,
});
const api: ManagementApi = admin.management;Session tokens
createSessionToken(options?): Promise<SessionToken>
Mint the short-lived credential a browser streams with. Call this from a server. It needs a full API key, and the whole point of the token it returns is that the full key never reaches a browser.
Requested lifetime. The box clamps it, currently to at most 600.
Returns { token, expiresAt, scope }. expiresAt is unix seconds. scope is
always tts: stream only, and it cannot mint another.
app.post("/api/avatar-token", requireLogin, async (req, res) => {
const { token, expiresAt } = await admin.management.createSessionToken({
expiresInSeconds: 300,
});
res.json({ token, expiresAt });
});Configuration
getSettings(): Promise<SettingsSnapshot>
The calling tenant's effective settings, with the model and text to speech
catalogues: { settings, models, effort_levels, reasoning_levels, tts_providers, persisted }.
updateSettings(patch): Promise<SettingsSnapshot & { ok, saved }>
Every field of SettingsPatch is optional: model, live_model,
system_prompt, max_tokens, top_p, temperature, effort, reasoning, tts_provider,
avatar, emotion_responsive, voice_id, plus per-provider voice ids.
getStatus(): Promise<BoxStatus>
Readiness of the two subsystems a reply needs:
{ llm, tts, ready, voice_origin, voice_provider, llm_state, tts_state, starting }.
clearConversation(): Promise<void>
Resets this caller's conversation. Only this caller's, never another tenant's on the same box.
Avatars
listAvatars(): Promise<AvatarCatalogue>
Every avatar on the box, with its tone variants and what is still preparing.
createAvatar(options): Promise<{ avatar, status, tones, note, active }>
Create an avatar from a portrait or a video.
The portrait or footage. A browser File, or a
Blob plus filename.
Names the avatar, because the box derives the id from the
filename. Two uploads called portrait.png are the same
avatar and the second overwrites the first, which is worth knowing before it
happens to you.
Tone variants to build.
Consent evidence for the likeness, where the box requires it.
Crop and framing for the generated clips.
How much the avatar moves.
Answers immediately with status: "preparing". A multi-tone build is minutes
of paid generation, so the upload is not held open for it. Poll
listAvatarClips for the outcome. The returned tones are the ones actually
coming, which may be fewer than you asked for, and note says why.
createPhotoAvatar(options): Promise<{ avatar_id, clips }>
Takes everything createAvatar does, plus a required avatarId. Unlike
createAvatar this one is synchronous on the box and holds the connection
until every clip is rendered, which can be minutes. It is also gated behind a
feature flag and a consent check on the photo, and throws ConfigurationError
where the box does not have it enabled.
deleteAvatar(avatarId): Promise<{ deleted, active }>
Deletes an uploaded avatar, never a stock one. A 404 means "not found, or not yours" and deliberately does not distinguish the two, so this cannot be used to discover what another tenant owns.
listAvatarClips(avatarId): Promise<AvatarClipList>
How each emotional variant is getting on. Accepts an avatar id or any of its
variant ids. Each AvatarClip carries
{ tone, state, variant_id, duration_s, updated_at, detail }, where state is
generated, generating, failed or not_requested.
A tone the box has never been asked for is absent rather than
listed as not_requested. Treat a missing tone as not requested,
not as an error.
retryAvatarClip(avatarId, tone)
Ask for one tone to be generated again. Answers 202, not 200: the build
takes minutes and the outcome arrives through listAvatarClips. A 409 means one
is already running for that tone, so it is a reason to wait rather than to retry
in a loop.
getAvatarClip(avatarId, tone?): Promise<BinaryAsset>
One rendered clip as MP4 bytes, { bytes, contentType }.
getAvatarPreview(avatarId): Promise<BinaryAsset>
A still frame as image bytes. A 404 has two meanings worth telling apart by their message: an unknown avatar, or one that exists but has not been prepared yet. The second is worth retrying and the first is not.
Voices
listVoices(): Promise<VoiceCatalogue>
Every voice the caller's engine offers.
previewVoice(voiceId?): Promise<BinaryAsset>
Audition a voice: a short WAV of a fixed sentence.
createVoice(options): Promise<{ ok, voice_id, voices, current }>
Clone a voice from a reference clip.
The reference clip. Must be at least 6 seconds of audio; the box measures it and refuses shorter ones rather than producing a bad clone.
Display name. The box rejects a blank one.
Needed when file is a bare Blob rather than a
File, since the box validates the extension.
Slow, and available only where the box runs the on-instance voice engine.
prepareVoice(voiceId): Promise<Record<string, unknown>>
Ask the engine to make a voice ready to speak. Call it after createVoice and
when switching voice, so a UI can show progress while the clone runs rather than
appearing to hang.
uploadAudio(file, filename?): Promise<{ name }>
Upload an audio file for the box to play.
Example: the whole server side
import { ZeliClient } from "@zeligate/zeli-avatar";
const admin = new ZeliClient({
serverUrl: process.env.ZELI_BOX_URL!,
apiKey: process.env.ZELI_API_KEY!,
});
// 1. build an avatar from a portrait
const created = await admin.management.createAvatar({
file: portrait,
filename: "acme-host.png",
tones: ["neutral", "confident"],
});
console.log(created.status, created.tones.join(", "), created.note);
// 2. wait for it, then check what actually rendered
await admin.waitUntilAvatarReady(created.avatar);
const { clips } = await admin.management.listAvatarClips(created.avatar);
for (const clip of clips) console.log(clip.tone, clip.state, clip.detail);
// 3. hand the browser a token, and only a token
const { token } = await admin.management.createSessionToken({ expiresInSeconds: 300 });