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.
| Method | Path | Purpose |
|---|---|---|
| GET | /auth/keys | This person's keys, without secrets |
| POST | /auth/keys | Create one. 201, and the only response that carries plaintext |
| DELETE | /auth/keys/{id} | Revoke one |
| POST | /auth/session/credential | Mint a short lived avatar server credential for a first party page |
List 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
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_..."
}
}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
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
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.