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.2. See what this deployment serves
Model backends are opt-in per environment. A model whose backend has no configured URL is hidden fromGET /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.
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.
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.
response_format: "verbose_json"
and the same call also returns speaker labels, per-segment timings and an
audibility read — see Speech to text.
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:
Never set audio.input.format
Never set audio.input.format
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.Send and expect 16 kHz, and check nothing enforces it
Send and expect 16 kHz, and check nothing enforces it
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.The two transcript events have different rules
The two transcript events have different rules
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.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.