Everything on a live socket is a JSON text frame with a type. This page is the reference for both directions. If you are writing a client from scratch, read it end to end; the two warnings near the bottom are the ones that cost production incidents.

The handshake

Four frames open every socket, in this order:
  1. eesi.synthetic_audio
  2. eesi.recording_disclosure
  3. eesi.session
  4. session.created
The first three are the gateway’s own, described below. It sends all three before it relays anything from the model, on both realtime endpoints and on every connection to them — a resumed session and a translation session included. Read until you see the type you want. A client that takes frame one as session.created reads a synthetic-audio marker instead, and the field it wanted is not there. session.created is the model’s opening frame, and it tells you what this end supports instead of making you probe for it.
Both extra fields are additive. An OpenAI-compatible client that ignores them is still correct; an EESI client reads them rather than guessing.

What you send

Anything else answers an error with type unknown_or_invalid_event. That includes eesi.keepalive on a gateway that does not advertise it — check the eesi.session greeting first.

What arrives

Three more are the gateway’s own. They open the socket, in the order below, before any model frame reaches you, and are namespaced eesi. so an OpenAI-compatible client can ignore all three:

The two events that read backwards

Both of these fail silently. You get a plausible transcript that is wrong.conversation.item.input_audio_transcription.delta is cumulative. Each delta carries the whole utterance so far, so it replaces the open entry. Append them and you get “hello hello there hello there world”.response.output_audio_transcript.done fires per segment, not per turn. The pipeline splits the reply at sentence boundaries and synthesizes each sentence separately, emitting a .delta and a matching .done for every one. Keep only the last .done and every multi-segment reply is truncated to its last sentence. Accumulate the deltas; treat done as a terminator. A reply ends at response.done, not at a .done.
The recorded event streams in spec/realtime/v1/ pin this behaviour, and every EESI client — the console, the iPhone app, the Mac engines, the gateway’s own recorder — replays that corpus. There is one reducer behind all of them, @eesi/realtime, but it is an internal package: it is not published, so write your own decoder against the rules above rather than reaching for it.

Legacy spellings

Older OpenAI variants spell four events without output_: This server emits only the canonical names. If your client also talks to a backend that uses the legacy ones, fold both onto one name at the edge of your decoder — handling one is how a client goes silent against exactly one backend.

A worked turn

What one exchange looks like on the wire, with a barge-in at the end:
Each flushed sentence produces a delta and a done carrying the same bytes, so a client that concatenates deltas and one that replaces on done see the same text — as long as the second one accumulates across segments. The reply is “It’s sunny. And mild.” Keeping only the last done gives you “And mild.”

Next

Turn-taking and barge-in

What decides speech_stopped, and what a cancelled turn owes your client.

Connections that survive

The eesi.session frames in full, and every close code.

Errors

Every failure this API produces, and which are worth a retry.

OpenAI compatibility

What an existing realtime client has to branch on.