API keys
Create one in the console under Developer → API keys. Name it after where it runs: once the secret is hidden, the name is the only thing that tells two keys apart. Send it as a bearer token:X-API-Key: sk-eesi-… is accepted everywhere too, for clients that reserve
Authorization for something of their own. Everything in these docs uses the
bearer form.
Keys are scoped to an organization, not to a user or a surface. A key reaches
every resource that organization owns and nothing that belongs to anyone else:
an id in a request body never implies ownership, because referenced resources
are re-fetched under the caller’s organization and answer 404 when they
belong to someone else.
What a key deliberately cannot do
Four actions refuse an API key and need a signed-in session in the console:
Each answers
403 with This action requires a signed-in session; API keys are not accepted. The SDKs have no method for any of them.
Archiving a key is deliberately not on that list. A key can archive keys,
including itself — containing a suspected leak should never wait on finding
somebody who can log in.
Listing and archiving
requests_30d comes back as null rather than 0 when the counter store is
unreachable — an unknown count and a genuinely idle key are different facts.
WebSocket credentials
A browser cannot set anAuthorization header on a WebSocket handshake, so the
gateway reads four channels. Send more than one and they are ranked in this
order — highest first:
Anything under a different name is
401 Missing authentication.
The query parameter beats the header, which is the opposite of what most
APIs do and of what
get_user_ws_flexible’s own docstring says. The
subprotocol is folded into the api_key slot, and get_user’s first branch
takes an API key ahead of anything in Authorization. If you send a key in
the URL for one client and a header for another, the URL is the one that
authenticates — check which credential a session actually ran under before
you go looking for the bug elsewhere.Tickets
Full request URLs are written to access logs and shipped to log stores that a much wider group can read than the person the credential belongs to. A reusable token in a URL is therefore a credential in three extra places. A value that is spent the first time it is presented is worth nothing to whoever reads the log afterwards. So the browser asks for a ticket over ordinary HTTPS, where the credential rides a header and nothing logs it, and puts the ticket in the socket URL:rt_, are single-use, and expire in 30 seconds —
expires_in says so on the response. That window covers minting and dialling,
nothing more, so mint one per connection and a fresh one for every reconnect.
Redemption is a single atomic read-and-delete, so two sockets cannot both win
with the same value.
A ticket is bound to one user in one organization, and to the API key that
minted it when there was one — the socket it opens is that key’s session. It is
redeemed only when it is the sole credential presented: send a ticket
alongside an Authorization header or an ?api_key= and the ticket is left
unspent while the other credential authenticates, which keeps the precedence
every existing client relies on unchanged.
A ticket under any other query name —
?ticket=, ?auth= — is a 401, not a
fallback. ?token= is also the channel a plain bearer token rides, so a
non-ticket value there is treated as one.Where a key goes, and where it does not
- One key per deployment. Staging, production and CI each get their own, so revoking one does not take down the others.
- Never in frontend code, and never in a repository. A key in a bundle is a key in every browser that loads the page.
- Never in a URL on the HTTP surface.
?api_key=exists for WebSocket clients that have no alternative; on HTTP, use the header. - Rotating is create-then-archive: both keys work while you cut over, and the old one stops the moment you archive it.
When it fails
On the OpenAI-compatible paths —
/v1/audio, /v1/chat, /v1/models,
/v1/realtime, /v1/voices — a 401 arrives in the OpenAI error envelope so
an OpenAI SDK reads it without a translation layer. On a socket the same
refusal is a 1008 close carrying the reason. See Errors.
Next
Quickstart
A key, a synthesized line, and a live conversation.
Connections that survive
Tickets, close codes, keepalives and resumption on a live socket.
Errors
Every failure, and which are worth a retry.
Compliance
Deletion routes, disclosure, and what a deployer owes.