Your server keeps the API key and mints a client secret for each player. The player’s device (a browser, a phone, a game build) opens the session with it, straight to EESI, and never sees your key. The session it opens is already the character you configured, and the device cannot change what you lock.
  1. The player’s device asks your server to talk to a character.
  2. Your server mints a secret with its API key (POST /v1/realtime/client_secrets) and sends the device the secret’s value (or secret.join from the SDKs).
  3. The device opens the session with it, over WebSocket or WebRTC, straight to EESI.
  4. Optionally, EESI posts each reply to your server, signed, and your server steers the session over a control channel.

Mint a secret

The response’s value is the secret. It opens one session, and expires after expires_after.seconds (10 to 600) if unused. The session itself lasts as long as any session, or max_duration_seconds. With the SDKs, character.clientSecret(...) (TypeScript) and character.client_secret(...) (Python) compose the session from your character, its memories and live state, and return secret.join: everything the device needs, and nothing it must not have.

What the device can and cannot change

eesi_lock names what is fixed; "all" fixes everything. A locked field in the device’s session.update is dropped, and the rest applies. A locked event is refused with an error whose code is session_locked. Even with everything locked, the device can still stream microphone audio, add the player’s own typed messages (conversation.item.create with a user message), ask for a reply (response.create, plain), and send tool results. eesi_limits, eesi_webhook, eesi_metadata and eesi_data are never part of the session configuration, so no client event can touch them.

Call length and silence

Microphone audio alone is not activity: a device streams silence continuously. The player speaking, a typed message, a request for a reply and the character answering all are. When a limit is reached the device receives
(reason is silence or max_duration), and the socket closes normally (1000). The SDKs end the session with reason hangup.

WebRTC

The same secret opens a WebRTC call, so phones and browsers get the platform’s echo cancellation and Opus. Post the SDP offer to /v1/realtime/calls with the secret where OpenAI’s clients put their ephemeral key:
The answer is application/sdp. Offer exactly one bidirectional Opus audio track and a data channel; events flow on the data channel as they do on the WebSocket. The call is the session the secret binds, with every lock, limit, webhook and data setting. The secret is spent by the first call or socket that presents it. In secret.join the SDKs include token and callsUrl for this.

Webhooks

With eesi_webhook, EESI posts the session’s conversation to your URL as it happens: what the player said or typed (input.completed), each finished reply (response.completed), and the session’s end with its whole transcript (session.ended). The URL must be public HTTPS; it is checked when you mint and resolved again before every post.
What the player said ("source": "speech") or typed ("text"):
When the session ends, with the whole conversation, oldest first:
EESI-Signature is v1= and the hex HMAC-SHA256, keyed with your secret, of {EESI-Webhook-Id}.{EESI-Webhook-Timestamp}.{raw body}. Check it over the raw body, and refuse a timestamp more than a few minutes old. The SDKs do both:
Answer with any 2xx. A failed post is retried three times over about 40 seconds; a 4xx other than 408 and 429 is not retried. Posts are in order, one at a time, and never hold up the conversation. Replies carry text; there is no per-reply audio URL.

Remember players across calls

A character can remember each player from one call to the next even though the player’s device holds the session. Your server keeps the memories (in your database, or a file), the SDK puts the player’s into each client secret it mints, and the session.ended webhook is what it learns from:
Memories are scoped to one player and one character: a player’s never reach another player’s session. rules say what may be kept, in your words. When a player deletes their account, sage.memory.erase({ playerId }) (Python: erase(player_id=...)) removes everything about them. This needs no WebSocket on your server, so it runs on Node 18 and later.

What the session’s audio and words are used for

eesi_data narrows what your organization’s own settings allow; it never widens them.
  • "train": false: the session is never used to train, evaluate or improve any model, whatever your organization grants now or later. It is marked when it opens, and every review, dataset and evaluation skips it.
  • "record": false: no recording or transcript is kept after the session, and the copy a dropped call would resume from is erased when it ends. The device is told it is not recorded (eesi.recording_disclosure).
Without them, a session follows your organization’s settings: sessions are recorded unless recording is turned off in Settings (and then, as with "record": false, no recording or transcript is kept after the session ends), recordings and transcripts are kept for your retention period (one year unless your organization set another), and none are used to improve Nur unless your organization turned on Help improve Nur.