By the end of this page you will have heard your own text spoken back, read it returned as a transcript, held one spoken turn with a live model, and looked at the record the conversation left. Ten minutes, most of it in step 5. Everything points at https://api.eesi.ai. The REST paths live under /v1.

1. Get a key

Open the console, go to Developer → API keys, and create one. Name it after where it will run — the name is the only way to tell keys apart afterwards.
The secret is shown once, at creation. Only a hash is stored, so a lost key cannot be recovered: create a replacement and archive the old one.
A key carries full access to its organization. Keep it on a server. See Authentication for the header forms and for how a browser gets a credential without ever seeing this one.

2. See what this deployment serves

Model backends are opt-in per environment. A model whose backend has no configured URL is hidden from GET /v1/audio/models and answers 503 model_unavailable on the REST routes — a deployment fact, not something you can fix in the request. An id no catalog entry has at all is 404 model_not_found. On /v1/realtime both cases collapse to the one 404. So an id that works in one deployment may be missing from another. Read the catalog rather than hardcoding.
The rest of this page uses nur-tts-v1, nur-stt-v1 and nur-realtime-v1. If one of them is missing from your list, pick the id of the same kind that is there.
The SDKs are not on PyPI or npm yet. Install them from a checkout — pip install -e sdk/python, or build and install sdk/typescript. Every step below also has a curl form that needs nothing installed. See SDKs.

3. Say a line

POST /v1/audio/speech takes model, input and voice. voice is a voice id from GET /v1/voices — or an OpenAI voice name like alloy, which resolves to the model’s own default so you can make a first request before you have a voice library. Ask for wav, because step 4 reads the file back.
Play it. Two things about that response are worth knowing now: nur-tts-v1 accepts only mp3, wav and pcm, and asking for a format it does not advertise is a 400 rather than a surprise container. And every audio response carries an X-Synthetic-Audio header declaring it machine-generated — you cannot turn it off, and it is a disclosure rather than a watermark. See Text to speech and Synthetic audio marking.

4. Read it back

POST /v1/audio/transcriptions is multipart: a file and a model.
You should get your sentence back. Ask for response_format: "verbose_json" and the same call also returns speaker labels, per-segment timings and an audibility read — see Speech to text.
Do not build a conversation out of steps 3 and 4. Slicing a microphone into repeated transcription requests destroys turn boundaries: a fixed slice cuts words in half and tells the model nothing about whether the person has finished speaking. That is what step 5 is for.

5. Hold a conversation

WS /v1/realtime is audio in, audio out, one model. The server decides when you have stopped talking, and it decides that from the timing of the frames as they arrive — so the audio has to be paced in real time, in small chunks, the way a microphone produces it. The wire is 16 kHz mono PCM16, base64 in input_audio_buffer.append. Frame size is yours to choose — 20 ms below, 100 ms in the console — and what matters is that frames arrive at the speed the words were spoken. You have no microphone in a terminal, so convert the file from step 3 to the wire rate and play it into the session at speaking speed:
reply.wav is the model answering the question you played it. The browser column is the shape of a real client; the working version, microphone capture and reconnect included, is in Build a voice agent in a browser. Three things break a live session silently, and all three are worth reading before you build on this:
The format field is a discriminated union whose PCM member admits only rate: 24000. An untagged {"rate": 16000} validates as mu-law instead and your PCM decodes as noise; a tagged PCM member at 16000 is rejected and the whole session.update — voice and instructions with it — is dropped while the socket stays healthy. Omit the field. The default is the 16 kHz the wire wants. See Live.
Nothing on the wire validates the rate you send. 24 kHz audio is accepted, resampled as though it were 16 kHz, and plays back half again too fast — which reads as a bad model rather than a bad client. session.created announces the rates the gateway accepts in capabilities.audio_input_rates; read it rather than assuming.
conversation.item.input_audio_transcription.delta is cumulative — each one carries the whole utterance so far and replaces the open entry. Appending them produces “hello hello there hello there world”. Meanwhile response.output_audio_transcript.done fires once per segment of a reply, not once per turn, so keeping only the last one truncates every multi-segment answer. See Realtime events.

6. Look at the record

Every live conversation leaves the same kind of row, whatever opened it.
There is no SDK method for this one, or for realtime, interact, story or memory — those are raw HTTP or a WebSocket. GET /v1/speech/sessions/{id} returns the conversation’s transcript with per-turn timing and the reply latency the caller actually felt. DELETE on the same path erases it. See Sessions.

What that cost

Audio meters at $0.001 per minute — synthesis, transcription and realtime at the same rate. Everything on this page is a few seconds of audio, so it rounds to nothing against the one-time signup grant, which is worth 1,440 minutes and does not renew. See Pricing and limits.

Next

Realtime interaction

Live and Duet — which one your problem is, and what they share.

Build a voice agent in a browser

The full client: tickets, capture, playback, barge-in and reconnect.

Errors

Every failure you will meet, and which ones are worth retrying.

Authentication

Keys, the two header forms, and browser tickets for a socket.