Zeli AvatarDeveloper docs

API reference / Engine

Engine

Most avatar APIs hand you a pointer to an engine somebody else runs. Ours does not, because the compute is yours: a GPU instance in your account, in a region you pick, that you start, stop and terminate. So this group is wider than a vendor's. It holds the WebRTC signalling itself, the box's own health, and the instance lifecycle.

Our signalling route is the engine, not a pointer to it

A vendor's engine call returns a host and a signalling endpoint and then your client opens WebRTC against that. POST /connect IS the signalling: you send an SDP offer and it answers with an SDP answer. There is no intermediate hop and no session id in the response.

MethodPathPurposeAuth
POST/connectOpen the persistent live stream, SDP in and SDP outany
POST/offerOne shot: lip sync a named audio fileany
GET/healthLiveness, active avatar, current speaking clippublic
GET/api/statsShutdown countdown plus CPU and GPU loadfull
GET/auth/gpu/statusIs there a box, and what may this caller launchPortal, administrator
POST/auth/gpu/startStart a stopped boxPortal, administrator
POST/auth/gpu/stopStop a running boxPortal, administrator
POST/auth/gpu/launchCreate a boxPortal, administrator and owner tier
POST/auth/gpu/terminateDestroy a boxPortal, administrator
GET/auth/gpu/diagnosticsWhy can nobody reach the boxPortal, administrator

Open the live stream

POST/connect

Opens the one persistent WebRTC session. The avatar idles on a silence clip and replies stream straight into the same connection, so there is no reconnect per message.

Opening a session closes any prior one, along with any one shot /offer sessions. One box, one live session, by design.

sdpstringRequired

Your WebRTC offer.

typestringRequired

The offer's type, normally offer.

avatarstringOptional

Which avatar to stream. Falls back to this caller's saved avatar, then to the box's active one.

silencestringOptional

An explicit idle clip. When you set this, it wins over the automatic choice.

client_contextobjectOptional

Sanitized facts about the person, such as their timezone and their name, for this session only. Validated field by field and dropped field by field on failure, so a malformed payload costs a nicety rather than the session. Nothing here is persisted.

{
  "sdp": "v=0\r\no=- ...",
  "type": "answer",
  "avatar": "01-presenter-male",
  "silence": "01-presenter-male__silence",
  "idle_stand_in": false
}

idle_stand_in is true when the idle face had to borrow somebody else's, because no prepared silent clip existed for the avatar you asked for. It is worth surfacing: the viewer sees one person idle and a different one speak.

One shot lip sync

POST/offer

Lip syncs an already uploaded audio file into a fresh WebRTC stream. There is no script and no text to speech in this path: upload the audio first with POST /api/upload, then name it here.

audiostringOptional

Filename of an uploaded audio asset. Falls back to the box's default clip. An unknown name is a 404.

avatarstringOptional

Which avatar to render.

emotionstringOptional

An explicit tone. Without one, and with emotion_responsive on, the tone is inferred from the audio.

emotion_responsivebooleanOptional

Whether to infer a tone from the audio. Defaults to this caller's setting, then the box's.

sdpstringRequired

Your WebRTC offer.

typestringRequired

The offer's type.

{
  "sdp": "v=0\r\no=- ...",
  "type": "answer",
  "avatar": "01-presenter-male__warm",
  "emotion": "warm",
  "transcript": "Thanks for waiting."
}

transcript is what the emotion inference heard, and is empty when the tone was given explicitly.

Box health

GET/health

The only route besides the page shell and static assets that takes no credential. Use it as a liveness probe.

{
  "status": "ok",
  "avatar": "01-presenter-male",
  "speaking_face": "01-presenter-male__confident"
}

Two facts, not one. avatar is the identity the user picked; speaking_face is the concrete clip playing right now, which changes every time a reply picks a tone. Conflating them reports a tone nobody chose.

Runtime stats

GET/api/stats

Requires a full key, and it is the only GET on the box that does, because the payload describes the machine rather than the product.

{
  "shutdown_minutes": 74,
  "cpu_percent": 12.4,
  "cpu_cores": 16,
  "gpus": [{ "name": "NVIDIA L4", "memory_used_mb": 9214, "utilization": 61 }]
}

shutdown_minutes counts down to the box's self termination and is null when no shutdown is scheduled. It is the field to watch: a box that is about to go is about to take every live session with it.

GPU lifecycle

A different plane, and a narrower gate

These six routes are on the portal plane, not the box. They take a signed in person rather than an API key, and they take a second administrator gate on top of that. An API key will not reach them, and they are callable from a browser only same origin. They are documented here because the instance lifecycle is genuinely part of this API, not because a key holder can drive it.

MethodPathWhat it does
GET/auth/gpu/statusReads the box and what this caller may launch
POST/auth/gpu/startStarts a stopped box
POST/auth/gpu/stopStops a running box
POST/auth/gpu/launchCreates the thing that bills. 201
POST/auth/gpu/terminateDestroys it. The only real off
GET/auth/gpu/diagnosticsAdds EC2 status checks and what the hostname resolves to

Every verb except the two reads is a POST even where a GET would be natural, so that no link prefetch and no drive by navigation can spend money.

All six answer the same body shape, so a client polls one thing:

{
  "gpu": {
    "instance_id": "i-0abc...",
    "state": "running",
    "instance_type": "g6.xlarge",
    "region": "ap-southeast-2",
    "availability_zone": "ap-southeast-2b",
    "public_ip": "13.x.x.x",
    "hostname": "box.avatar.example",
    "launched_at": 1769990000,
    "running_seconds": 4820,
    "max_runtime_seconds": 7200,
    "auto_stop_at": 1769997200,
    "over_budget": false,
    "hourly_cost_usd": 0.8048,
    "ephemeral": true,
    "can_launch": true,
    "bootstrap_configured": true
  },
  "owner": false,
  "regions": [
    {
      "id": "ap-southeast-2",
      "label": "Sydney",
      "data_resident": true,
      "can_serve": true,
      "smoke_test": false,
      "types": ["g6.xlarge", "g6.2xlarge"],
      "owner_types": []
    }
  ]
}

owner and regions sit beside the box rather than inside it, because they describe the caller rather than the instance: they are how a page learns which controls to offer. regions with one entry means there is no choice to make.

POST /auth/gpu/launch additionally accepts instance_type and region in the body. Both are optional, and both are re decided server side against the verified identity and the deployed environment. A client's copy of the rule chooses what to show, never what is allowed.

The refusal vocabulary is deliberately granular, so a page can say what happened rather than "something went wrong":

Statuscode
409gpu_already_running, gpu_not_startable, gpu_region_not_allowed
403gpu_type_owner_only
400gpu_type_not_allowed
503gpu_no_capacity, gpu_quota_exceeded, gpu_launch_not_configured
502gpu_unavailable

Not served in this group

  • A managed engine pool. There is no region routing policy, no engine version negotiation and no billing pre check, because there is no pool: there is one box per environment, and its DNS name is re pointed at whatever instance is current.
  • A session id in the signalling response. /connect answers with an SDP answer and nothing that identifies the session, which is why nothing downstream can be addressed per session.
Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source