API reference / Avatar videos
Avatar videos
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.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/avatar/clips | Every tone this box has a view on, for one avatar | any |
| POST | /api/avatar/clips/retry | Generate one tone. 202 | any |
| GET | /api/avatar/clip | One rendered clip, as MP4 bytes | any |
Two routes that look adjacent and are not
Worth naming before you map them by their names:
GET /api/videoslists avatar source uploads, not rendered jobs. It is documented under Avatars.POST /offerlip 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
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_..."const { clips } = await admin.management.listAvatarClips("acme-host");{
"avatar": "acme-host",
"clips": [
{
"tone": "confident",
"state": "generated",
"variant_id": "acme-host__confident",
"duration_s": 4.2,
"updated_at": 1769903600,
"detail": ""
}
]
}state | Meaning |
|---|---|
generated | The clip exists and is streamable |
generating | A build is in flight. It takes minutes |
failed | The build ran and did not produce a clip. detail says what happened |
not_requested | This tone was never asked for |
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
The avatar id, or any of its variant ids.
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"}'await admin.management.retryAvatarClip("acme-host", "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.
| Status | code | When |
|---|---|---|
404 | unknown_avatar | Not an avatar on this box, or not yours |
400 | unknown_tone | This deployment builds no tone by that name |
501 | avatar_clips_unavailable | This box does not generate emotion clips |
403 | consent_required | No attestation on record, or the photograph it attested to is gone |
409 | clip_already_generating | That exact variant is already building. Wait, do not loop |
503 | clip_generator_unavailable | The generator is not reachable right now |
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
The avatar id.
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 noGET /avatar-videosto page through and nostatus,createdAfterorcreatedBeforefiltering. - 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
409instead: a second call for a variant already building is refused rather than charged twice.