Zeli AvatarDeveloper docs

API reference / API keys

API keys

Create and revoke keys from your signed-in account. The secret is shown only when a key is created; store it securely on your backend.

These account routes require a signed-in portal session or identity token. An API key cannot create another account key. For browser streaming, use short-lived session tokens instead of exposing a full key.

MethodPathPurpose
GET/auth/keysThis person's keys, without secrets
POST/auth/keysCreate one. 201, and the only response that carries plaintext
DELETE/auth/keys/{id}Revoke one
POST/auth/session/credentialMint a short lived avatar server credential for a first party page

List keys

GET/auth/keys

Takes no parameters. Returns only the signed-in account's keys.

{
  "keys": [
    {
      "id": "3fa7c1...",
      "name": "production backend",
      "hint": "zsk_live_...c41f",
      "last4": "c41f",
      "created_at": 1769900000,
      "created_by": "you@example.com",
      "revoked": false,
      "revoked_at": null,
      "revoked_by": null
    }
  ]
}

id is the key's digest, not the key. Revoked keys stay in the list with revoked: true and the time and actor recorded, because a key that vanishes leaves nothing to audit against.

Create a key

POST/auth/keys
namestringRequired

A label you will recognise later. An invalid one is a 400 with code invalid_name.

One developer can hold at most 100 keys, and revoked keys count towards that number. Creating one more is a 409 with code key_limit_reached and a message you can show as it is.

Response 201. The same record as above, plus one field:

{
  "key": {
    "id": "3fa7c1...",
    "name": "production backend",
    "hint": "zsk_live_...c41f",
    "last4": "c41f",
    "created_at": 1769900000,
    "created_by": "you@example.com",
    "revoked": false,
    "secret": "zsk_live_..."
  }
}
Reveal once

secret appears in this response and in no other. It is not stored in a form anything can read back and it is not re derivable. Copy it now, or create another key.

Revoke a key

DELETE/auth/keys/{id}

Response 200: { "ok": true }.

A 404 with code key_not_found means no such key of yours, and it is also the answer for an id that never existed. That is deliberate: a 403 would confirm the id belongs to somebody, which turns this route into a probe for other developers' key ids.

A short lived avatar server credential

POST/auth/session/credential

The one bridge between the two authentication planes. It issues a signed in person a credential the avatar server accepts, so a first party page never has to ask for a pasted key.

A POST and not a GET, for the same reason key creation is: minting a live credential must not be reachable by a navigation, a link prefetch or an image tag. It sits under /auth/session/ rather than /auth/keys/ so it can never be confused with a person's own key management.

Response 201:

{
  "credential": {
    "secret": "zsk_...",
    "expires_at": 1769903600,
    "expires_in": 900,
    "scope": "full"
  }
}

Both clocks are given on purpose. expires_at is absolute epoch seconds, which is what the avatar server enforces; expires_in is seconds from now, which is what a browser should time against, so a skewed client clock cannot renew late.

How a key is presented

Once you hold one, every avatar server route accepts it in one of three forms, in this order of preference:

curl "https://zeli-avatar.zelibot.xyz/api/avatars" -H "X-Api-Key: zsk_live_..."
curl "https://zeli-avatar.zelibot.xyz/api/avatars" -H "Authorization: Bearer zsk_live_..."

The third, ?session_token= on the URL, exists only because a browser WebSocket upgrade cannot set headers. Use it there and nowhere else: a credential in a query string lands in access logs, referrer headers and browser history.

For a browser, do not send a full key at all. Mint a stream only token on your server with POST /v1/streaming.create_token and send that. See Authentication for the whole picture.

Signing in

Three more routes exist on the portal plane and are part of the sign in flow rather than of this API: POST /auth/request-link starts a magic link sign in and always answers the same neutral message so it cannot be used to discover which addresses have accounts, GET /auth/callback redeems the emailed link, and POST /auth/logout clears the session. POST /auth/session/exchange trades a main pool identity token for the portal's own cookies.

Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source