Zeli AvatarDeveloper docs

API reference / Session settings

Session settings

These defaults are separate from named Personas. Use GET /auth/settings and POST /auth/settings with a signed-in account or full API key for settings without an active stream. The compatible /api/settings routes below are available during a running avatar session. A streaming token cannot save settings.

Read the record

GET/api/settings

Takes no parameters. Whose settings these are comes from the credential, never from the request.

curl "https://zeli-avatar.zelibot.xyz/api/settings" -H "X-Api-Key: zsk_live_..."
{
  "settings": {
    "provider": "anthropic",
    "model": "au.anthropic.claude-sonnet-5",
    "live_model": "au.anthropic.claude-haiku-4-5",
    "system_prompt": "You are a friendly front desk host.",
    "max_tokens": 1024,
    "top_p": null,
    "temperature": null,
    "effort": "medium",
    "reasoning": "low",
    "tts_provider": "cartesia",
    "voice_id": "a0e99841-...",
    "voice_id_chatterbox": null,
    "avatar": "01-presenter-male",
    "emotion_responsive": true
  },
  "models": [],
  "effort_levels": ["low", "medium", "high", "xhigh", "max"],
  "reasoning_levels": ["off", "low", "medium", "high"],
  "tts_providers": [],
  "persisted": true
}
FieldMeaning
settingsThis caller's effective record. The whole of it
modelsThe model catalogue, documented under LLMs
effort_levelsReasoning effort values the models accept, low to high
reasoning_levelsHow long the model may think before it speaks, off to high
tts_providersThe voice engines this deployment can run, with their capability flags
persistedWhether the settings store is reachable. false means a save will not survive the avatar server
This route is `any` scope on purpose, and it carries your prompt

The payload includes system_prompt, which is why it is scoped to the caller rather than to the box. It used to answer everybody with whoever had configured the machine last, which on a shared avatar server meant handing out another tenant's persona.

Write the record

POST/api/settings

Requires a full key. Send only the fields you want to change; the rest are left alone. Writes nothing to process state, so a save landing during somebody else's reply cannot change it, and cannot change a reply of your own already in flight.

The settable fields are an allowlist, not "whatever was posted":

modelstringOptional

The quality model, used for a one shot turn.

live_modelstringOptional

The low latency model, used for the always on live conversation.

system_promptstringOptional

The persona's instructions. This is the entire knowledge surface; see Knowledge.

max_tokensintegerOptional

Reply length ceiling.

top_pnumberOptional

Nucleus sampling. Only accepted by models whose supports_sampling flag is true.

temperaturenumberOptional

Sampling temperature. Same capability gate as top_p.

effortstringOptional

One of effort_levels. max is accepted only by models flagged effort_max. Used only while reasoning is unset; once a thinking level is chosen, that level sets the effort.

reasoningstringOptional

How long the model thinks before it speaks: one of reasoning_levels (off, low, medium, high). Higher levels think longer before speaking, so replies start later. An empty string or null is automatic: each model's own default, shown as default_reasoning in the model catalogue (low on Claude Opus 5 and Sonnet 5, which think unless told otherwise; off elsewhere). A value that is not a level is treated as automatic rather than refused. The level is chosen for the live_model; the other model takes it only when it takes a level the same way, and otherwise runs at its own default. A model with no thinking control ignores it.

tool_phrasesobjectOptional

What the avatar says before a tool runs when the model said nothing first, in this persona's own words: {tool name: {language: line}}, for example {"end_call": {"en": "Bye for now."}}. Languages are en es fr de it pt hi ja; a line is one sentence of at most 160 characters, with no angle or square brackets; at most 32 tools. The line is chosen in the call's current language, then the built in line for that language, and English only as a last resort. skip_turn cannot have one: it is silent on purpose. A refused value answers 400.

tts_providerstringOptional

Which voice engine to speak with.

avatarstringOptional

The default avatar for this caller's sessions.

emotion_responsivebooleanOptional

Whether to infer a delivery tone rather than always speaking neutrally.

voice_idstringOptional

The Cartesia voice slot, kept under this name for compatibility. Every other provider gets voice_id_<provider>, matched by prefix.

toolsarrayOptional

This persona's custom tools, at most ten. Written whole: send the full list each time, and an empty list clears it. The definition fields and their limits are on the Tools page.

curl -X POST "https://zeli-avatar.zelibot.xyz/api/settings" \
  -H "X-Api-Key: zsk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"system_prompt": "You are a friendly front desk host.", "effort": "medium"}'

Response 200:

{ "ok": true, "saved": true, "settings": {} }

saved is whether the record reached durable storage. settings is the whole record after the write, so one round trip is enough.

A refusal is a 400 with ok: false and a reason, not a 500. There are two reasons, and the message names which one it is:

{
  "ok": false,
  "error": {
    "code": "setting_refused",
    "message": "..."
  }
}

A model that does not exist, or one this deployment will not send data to. The second is the residency rule: see the selectable flag under LLMs.

Give the persona tools

A persona's custom tools live on the same record, under tools. Defining one is a settings write, so it needs a full key and belongs on your server. Answering one happens wherever the session runs, through the SDK.

// Server side, full key: define the tool.
await admin.management.updateSettings({
  tools: [
    {
      name: "book_a_tour",
      description: "Book a tour of the building. Call it once the person has named a day.",
      parameters: {
        type: "object",
        properties: { day: { type: "string" } },
        required: ["day"],
      },
      mode: "await_result",
      timeout_seconds: 8,
    },
  ],
});
 
// Where the session runs: answer it. Register before connect.
client.registerToolCallHandler("book_a_tour", async ({ day }) => {
  const booking = await tours.book(String(day));
  return { confirmed: true, at: booking.time };
});

The read returns the list under settings.tools. The persona only ever sees the entries that are enabled and valid. A refused definition answers 400 setting_refused and saves nothing from that request.

A tool result is untrusted text to the model

Whatever a handler returns is read by a language model, and so is the message of a ToolCallError it throws. Any other error is kept off the wire and sent as "The tool failed." Return the smallest answer that does the job, never put a secret in a tool's arguments, description or result, and authorise inside the handler rather than trusting the arguments. The full list is under Tools.

Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source