Get started / Authentication
Authentication
The avatar server authenticates with an API key presented as
X-Api-Key. Auth is configured on the box, so a public deployment
requires a key while an embedded loopback box can run open. The wire schema
mirrors HeyGen (X-Api-Key, streaming.create_token) so
migrating an existing integration is familiar.
When auth is on
Server auth is off unless you configure keys. Set one of these on the box:
ZELI_API_KEYS— a comma-separated list of full API keys.ZELI_API_KEYS_TABLE— a DynamoDB table of keys (adds session-token minting).
If neither variable is set, the server is open — correct for a loopback/embedded box (the on-instance avatar for the AI pipeline), wrong for a public one. A standalone / public box must set one of the two before it is reachable on a network.
Presenting a key
Send your key on every request as X-Api-Key (an Authorization: Bearer <key>
header is also accepted). The SDK sends it for you when you pass
ZeliClient(api_key=...).
from zeli import ZeliClient, AvatarConfig, ClientOptions
# The SDK forwards the key on signalling + the control WebSocket.
client = ZeliClient(
api_key="zeli_...",
avatar_config=AvatarConfig(avatar_id="presenter-male-1080"),
options=ClientOptions(server_url="https://avatar.zeligate.ai"),
)curl -X POST "https://avatar.zeligate.ai/v1/streaming.create_token" \
-H "X-Api-Key: zeli_..." \
-H "Content-Type: application/json" \
-d '{"ttl_seconds": 300}'Full keys vs. session tokens
There are two credential kinds, with different scope:
| Credential | Scope | Minted by | Can mutate? |
|---|---|---|---|
| Full key | Everything | Configured on the box | Yes |
Session token (zsk_temp_…) | Stream-only (tts) | A full key | No |
A full key calls everything. A session token is stream-only — it can drive an avatar stream but never mutate server state. This is what you hand to a browser, so a real key never leaves your backend.
Minting a session token
Your backend calls streaming.create_token with a full key and hands the
short-lived token to the browser.
Lifetime of the token in seconds. Clamped to the range 30–600.
// Request (full-key gated — X-Api-Key: zeli_...)
{ "ttl_seconds": 300 }
// Response
{
"data": {
"token": "zsk_temp_...",
"expires_at": 1770000000,
"scope": "tts"
}
}The browser then passes the token on the /connect signalling request and on the
control WebSocket as a ?session_token=... query parameter:
POST /connect?session_token=zsk_temp_...
WSS <control-gateway>?session_token=zsk_temp_...A leaked session token can drive a stream until it expires, but it can never
mutate — a mutation attempt returns 401 insufficient_scope. Expiry
is enforced server-side on every request.
Error envelope
Auth failures return 401 with a small JSON envelope:
{ "code": "missing_api_key", "message": "..." }| Status | code | When |
|---|---|---|
401 | missing_api_key | Auth is required but no key was sent |
401 | invalid_api_key | The key sent isn't recognized (or has expired) |
401 | insufficient_scope | A session token was used on a mutating / management route |
In the SDK, a rejected key or token surfaces as
AuthenticationError.
Provisioning a public box
- Set
ZELI_API_KEYS(orZELI_API_KEYS_TABLE) on the box. - Terminate TLS in front — clients expect
https:///wss://. - For browser streaming, mint short-lived session tokens with
streaming.create_tokenfrom your backend; never ship a full key to the page.