Zeli AvatarDeveloper docs

API reference / Avatar videos

Avatar videos

We do not render a script to a video file

There is no route that takes a script, speaks it and hands back a downloadable MP4. Nothing on this service writes an MP4 of spoken audio at all. If that is what you are looking for, stop here: the honest answer is that it does not exist, and the rest of this page is a different thing that shares the word "video".

What we do generate is emotion tone clips: short, silent, looping renders of one avatar in one delivery tone, built once and then lip synced at speaking time. Generating one is a real asynchronous job, with the same submit, poll and fetch shape a rendering API uses, so it is documented that way.

MethodPathPurposeAuth
GET/api/avatar/clipsEvery tone this box has a view on, for one avatarany
POST/api/avatar/clips/retryGenerate one tone. 202any
GET/api/avatar/clipOne rendered clip, as MP4 bytesany

Two routes that look adjacent and are not

Worth naming before you map them by their names:

  • GET /api/videos lists avatar source uploads, not rendered jobs. It is documented under Avatars.
  • POST /offer lip syncs a pre uploaded audio file into a live WebRTC stream. There is no script in it, no text to speech in it, and it leaves no artefact behind. It is documented under Engine.

List clip states

GET/api/avatar/clips
avatarstringRequired

An avatar id, or any of its variant ids. Query parameter.

curl "https://avatar.zelibot.xyz/api/avatar/clips?avatar=acme-host" \
  -H "X-Api-Key: zsk_live_..."
{
  "avatar": "acme-host",
  "clips": [
    {
      "tone": "confident",
      "state": "generated",
      "variant_id": "acme-host__confident",
      "duration_s": 4.2,
      "updated_at": 1769903600,
      "detail": ""
    }
  ]
}
stateMeaning
generatedThe clip exists and is streamable
generatingA build is in flight. It takes minutes
failedThe build ran and did not produce a clip. detail says what happened
not_requestedThis tone was never asked for
Sparse on purpose

A tone this box has never been asked for is absent from the array rather than listed as not_requested. That keeps the tone vocabulary in one place and stops the box asserting opinions about tones it has never heard of. Treat a missing tone as not requested, not as an error.

A 404 means no such avatar on this box, of yours. Not found and not yours are the same answer.

Generate one tone

POST/api/avatar/clips/retry
avatarstringRequired

The avatar id, or any of its variant ids.

tonestringRequired

A tone this deployment builds. Anything else is a 400.

curl -X POST "https://avatar.zelibot.xyz/api/avatar/clips/retry" \
  -H "X-Api-Key: zsk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"avatar": "acme-host", "tone": "confident"}'

Response 202, not 200, because the answer does not exist yet. A build is minutes long and the outcome arrives through GET /api/avatar/clips:

{
  "avatar": "acme-host",
  "tone": "confident",
  "variant_id": "acme-host__confident",
  "state": "generating"
}

Refusals, in the order they are checked

Every refusal happens before a generator is constructed, because each call is about a minute of metered generation.

StatuscodeWhen
404unknown_avatarNot an avatar on this box, or not yours
400unknown_toneThis deployment builds no tone by that name
501avatar_clips_unavailableThis box does not generate emotion clips
403consent_requiredNo attestation on record, or the photograph it attested to is gone
409clip_already_generatingThat exact variant is already building. Wait, do not loop
503clip_generator_unavailableThe generator is not reachable right now
Owning the avatar is not the same as having attested

Animating a likeness again is a new animation, so it goes through the same consent gate the original upload did: the attestation from the ledger, the owner from the live credential, and the photograph re hashed against what was agreed to. A video upload, and an upload whose consent never reached the box, both leave a record with an owner and no attestation, and neither may generate a clip.

Fetch a clip

GET/api/avatar/clip
avatarstringRequired

The avatar id.

tonestringOptional

Which tone. Defaults to the deployment's default tone.

Returns MP4 bytes, not JSON, and is authenticated like every other box route. That is why a browser fetches it into a blob rather than pointing a video element at it: the alternative is a live credential in a query string, which lands in access logs, referrer headers and history. The response is marked private caching, because the edge in front of a box is shared.

Errors use the structured envelope: { "error": { "code": "...", "message": "..." } }.

The build journal

Clip builds also report through builds on GET /api/avatars, keyed by avatar. That key is on a list route rather than on the create response for a structural reason: the upload answers in milliseconds and the build takes minutes, so the create response is written before the outcome exists. A client polling the avatar list gets the answer on a request it was already making.

Not served in this group

  • Script to video. Named again because it is the thing most people arrive looking for. No route accepts a script, and no route returns a rendered file of spoken audio.
  • A job id, a job list and job filters. A clip is addressed by (avatar, tone) rather than by a job identifier, so there is no GET /avatar-videos to page through and no status, createdAfter or createdBefore filtering.
  • Cancel. The in flight claim is released only by the build task itself. No request can release it.
  • An expiring download URL for a clip. The bytes come back from the box directly. The presigned URL mechanism does exist and is used for avatar media on the portal plane; it is not wired to clips.
  • An idempotency key header. It appears nowhere on this surface. Use the 409 instead: a second call for a variant already building is refused rather than charged twice.
Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source