API reference / API keys
API keys
Self service key management, served from the always on portal plane. This is where the credential every other page in this reference assumes comes from. Comparable APIs keep key creation in a web console with no route behind it; ours is an API, so it is documented.
These routes take a signed in person, by main pool identity token or by the portal's session cookie, not an API key. They are callable from a browser only same origin. The key they hand back is what the box routes then accept.
| 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 box credential for a first party page |
List keys
Takes no parameters. The acting identity is the partition: the request names no owner, which is what makes another developer's keys unreachable rather than merely filtered.
{
"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 box credential
The one bridge between the two authentication planes. It issues a signed in person a credential the box 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 box 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 box route accepts it in one of three forms, in this order of preference:
curl "https://avatar.zelibot.xyz/api/avatars" -H "X-Api-Key: zsk_live_..."
curl "https://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.