Zeli AvatarDeveloper docs

API reference / Personas

Personas

There is no persona table here. A caller has one settings record, it has no id and no name, and every session that credential opens uses it. Two routes read and write it, and the fields it holds are most of what a persona is elsewhere: the prompt, the model, the voice, the avatar, the sampling knobs.

One record, not many

There is no list, no create, no id addressed read, no update by id and no delete, because there is nothing to address: the subject of both routes is the credential the request was already authenticated with, and it is never named in a path, a query parameter or a body field. Building named personas is a new table rather than a field rename, so it is not described here as if it were nearly done.

MethodPathPurposeAuth
GET/api/settingsThis caller's effective settings, plus the cataloguesany
POST/api/settingsSave this caller's settingsfull

Read the record

GET/api/settings

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

curl "https://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 box
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 box 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://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.

Not served in this group

  • GET /personas, POST /personas, GET, PUT and DELETE by id. There is one unnamed record per credential, so there is nothing to list and nothing to address.
  • A public widget configuration read. There is no widget, and no unauthenticated read of anything.
  • Nested avatar and voice objects on the record. Our fields are flat scalars: avatar is an id string and the voice is a per provider id slot.
Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source