Credentials on a socket
The gateway reads four credential channels. Send one. When more than one is present, this is the order that decides which is used:
An
api_key beats a header, and a header beats token — which is why a ticket
sent alongside either of them is never redeemed. If you can set a header, set
the header and stop reading this section.
Tickets, for clients that cannot set headers
A browser cannot put a header on a WebSocket handshake, so the credential has to ride the URL — and URLs are written to access logs. The fix is not to avoid the URL; it is to put something in it that is worthless once used.How a session is refused or ended
A connect-time refusal and a mid-call close do not look alike, and the reason a reader arrives here — a 403 nothing explains — is not a close code at all.Refused at the handshake
Everything the gateway checks beforeaccept() is answered by closing the
socket while it is still an HTTP request, which uvicorn turns into a 403
handshake rejection. There is no close code and no reason text, whatever the
gateway computed internally. A browser surfaces it as a failed connection and a
close with code 1006; websockets raises InvalidStatus: server rejected WebSocket connection: HTTP 403.
Because a 403 carries nothing, check these against the HTTP API before you go
looking in the socket: the same key on
GET /v1/audio/models tells you whether
the credential works and whether the model exists, and answers a proper JSON
error when it does not.
Closed after it opened
Once the socket is accepted, every ending is a close code.1008 and 1011 reach a client identically and mean opposite things. 1008 is a
limit, and “try again in a moment” is sometimes honest. 1011 is a failure, and
telling a user the service is busy sends them into a retry loop that cannot
succeed while hiding an outage from you. Render them differently.
A reconnect policy that works
The policy every EESI client shares, and a reasonable default for yours:- Do not redial a session that never established — that first failure is yours to report, not to paper over.
- Do not redial after 1000, 1002, 1003 or 1008.
- Otherwise redial with equal-jitter exponential backoff: a window doubling from 1 second to a maximum of 8, wait half the window plus a random half. The fixed half keeps a returning deploy from being hammered; the random half stops every dropped session from coming back in lockstep.
- Give up after six attempts.
Keep a quiet session open
A session ends by itself at three hours of wall clock, or after five minutes without a frame from you. The idle timer watches your frames only. Upstream traffic does not renew it, because a backend happily talking into a silent socket is exactly the case being reclaimed. A session streaming microphone audio renews itself continuously and never reaches the timeout. A text-only session does not: nothing goes up the socket while the person reads, and five minutes later the slot is reclaimed from someone who was still using it. Send a beat instead:keepalive relays the event to the model instead of
consuming it, and the model answers with an unknown_or_invalid_event error.
Resume a lost session
Every session’s greeting carries a gateway-privateeesi.session event with a
resumable identity. Keep both values.
session_id and token, so it can be resumed
again, and adds "resumed": { "replayed_items": N }.
A resume that misses is not a refusal. An expired checkpoint, a token that does
not match, or a session belonging to someone else all get you a fresh
session with a new handle in the greeting, because clients treat a 1008 as
final and would abandon a conversation the person is still trying to have.
Read
resumed to know which one you got.What is kept, and what is not
Checkpoints live for 15 minutes after the last turn. A keepalive extends that while somebody is there. A session closed deliberately (1000 from your client) is not resumable — only a lost one is. Kept: the mergedsession.update configuration, and a tail of complete
transcript entries — at most 200 entries and 256 KiB of text, oldest whole
entries dropping first. Retained entries are never sliced.
Not kept: earlier context outside that window, the turn in flight, audio,
images, and tool outputs.
If one entry exceeds 64 KiB, or the configuration exceeds 64 KiB, resumption is
disabled rather than silently altering the words a future model would see. The
gateway clears the checkpoint, sends eesi.session with
resume.supported: false, and emits a recoverable error:
The frame that says why a call ended
The balance and the free-tier day are re-checked once a minute while a session is up, not only at connect, so a 1008 close can arrive minutes into an established conversation. Immediately before it does, the gateway sends:resume is withdrawn because a resume would be refused for the same reason —
drop the handle instead of redialling into it. limit.code is
free_tier_daily_limit or insufficient_quota. The same text becomes the
close frame’s reason, where it is cut to 120 characters — so read it from the
frame, not from the close.
Show that message. It is the difference between a user who tops up and a
user who thinks your product is broken. A re-check that cannot decide — the
ledger or the ceiling store unreachable — refuses a new session but never
cuts a live one.
Surviving a deploy
A rolling deploy is the most common way a healthy session dies. The orchestrator drains a worker first: it polls the worker’s live-session count and waits for zero before sending SIGTERM. A session still up when the signal lands is force-closed with 1012, which is exactly what resume is for — redial with the handle and the conversation continues on a new pipeline. Sessions longer than an hour need care in a cluster that caps connection draining at an hour. Resume covers the gap; a client that does not implement it will drop those calls.Next
Authentication
Keys, what they cannot do, and the console surfaces that manage them.
Errors
Every HTTP and socket failure in one table.
Build a voice agent in a browser
Ticket minting and the reconnect path in working code.
Pricing and limits
What the ceilings above cost you when you reach them.