Every call outside the console carries an API key. A WebSocket cannot always carry a header, so it has its own credential channels — that is the one place where authentication here differs from any other REST API, and this page covers both.

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.
The secret is shown in full exactly once, at creation. Only a hash is stored and only the last four characters are recoverable, so a lost key cannot be retrieved — create a replacement and archive the old one.
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 an Authorization 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.
The subprotocol channel exists so an unmodified OpenAI browser client connects, and OpenAI’s own name for it says what it is. It puts a reusable, organization-wide key inside a page. Use a ticket instead.

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:
Tickets start with rt_, are single-use, and expire in 30 secondsexpires_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.