Realtime audio
EVA’s developer transport uses a WebSocket carrying JSON controls and binary audio. It serves published EVA Speech Engine versions.
Connection sequence
- Create a conversation using your server-side API key.
- Connect to the returned
stream_url, adding?token=and its URL-encodedstream_token. - Immediately send
session.startwith the audio format andserver_audio_versions: [1]. - Wait for
session.startedand checkserver_audio_version. - Send microphone PCM and process incoming events/audio concurrently.
- Send
session.endwhen the caller finishes.
The token is single-use and expires according to the bootstrap response. Redact it from logs. The developer route does not emit the dashboard-only stream.authenticated barrier.
Input format
Send raw PCM signed 16-bit little-endian, 16,000 Hz, one channel as binary messages. Do not send MP3, Opus, a WAV container, or base64 text. Resample device audio before sending.
For example, a 20 ms chunk contains 320 samples / 640 bytes. Pace chunks in real time and bound your queue so delayed audio cannot accumulate indefinitely.
JSON controls include:
| Type | Use |
|---|---|
session.start | Initial handshake; see quickstart |
session.end | Finish the conversation |
playback.completed | Report that a generation’s audio has actually finished playing; include generation_id |
Input stream telemetry and generation envelopes used by the dashboard are optional extensions; raw PCM is accepted by the developer stream.
Assistant audio framing
Offer version 1 with "server_audio_versions": [1]. A session.started response with no supported version indicates a negotiation failure. Read the output format from the event’s audio_format.
Every version-1 binary output message begins with this little-endian header:
| Byte offset | Size | Meaning |
|---|---|---|
| 0 | 4 | ASCII magic EVAO |
| 4 | 1 | Version 1 |
| 5 | 1 | Flags, currently 0 |
| 6 | 2 | UTF-8 generation ID length, 1–128 |
| 8 | 4 | Sequence number, starting at 1 per generation |
| 12 | Variable | Generation ID bytes |
| Following ID | Remaining bytes | PCM16 audio, nonempty and an even byte count |
A decoder for Node.js:
js
export function decodeEvaAudio(buffer) {
if (buffer.length < 12 || buffer.toString('ascii', 0, 4) !== 'EVAO' ||
buffer[4] !== 1 || buffer[5] !== 0) throw new Error('Invalid EVA audio');
const idLength = buffer.readUInt16LE(6);
const sequence = buffer.readUInt32LE(8);
const end = 12 + idLength;
if (idLength < 1 || idLength > 128 || sequence < 1 || end >= buffer.length ||
(buffer.length - end) % 2 !== 0) throw new Error('Invalid EVA audio');
const generationId = new TextDecoder('utf-8', { fatal: true })
.decode(buffer.subarray(12, end));
return { generationId, sequence, pcm: buffer.subarray(end) };
}Track sequence numbers within each generation. Do not play the header or ID as audio. Omitting server_audio_versions selects a transitional raw-PCM mode; use version 1 for new integrations so stale generations can be identified.
Events and interruption
JSON events include conversation_id and a type. Turn and generation events also identify the relevant turn_id and/or generation_id.
| Event | Client behavior |
|---|---|
session.started | Configure playback using the negotiated audio format |
user.speech_started / user.speech_ended | Update the caller speaking state |
transcript.partial | Show provisional user text |
transcript.final | Update user text using final_revision |
turn.user_committed | Record the committed user turn |
assistant.generation_started | Begin tracking the new generation |
assistant.text_delta / assistant.text_final | Update assistant text |
assistant.audio_started | Assistant audio is beginning |
assistant.interrupted | Stop and clear queued audio for that generation; ignore later stale chunks |
assistant.audio_exhausted | No more audio will arrive for this generation; drain queued playback |
assistant.completed | Assistant turn completed; do not cut queued audio prematurely |
conversation.ending / conversation.completed | Track conversation lifecycle |
conversation.failed / error | Surface a safe failure and finish the session as appropriate |
Keep receiving while sending audio. Barge-in requires clearing local playback when interrupted; stopping generation on the server alone does not remove audio already buffered by your client.
Send playback.completed only after the relevant generation’s final queued sample has played. This helps natural ending behavior. A text-final event is not proof that audio has finished.
Failure recovery
Treat disconnects as terminal for that stream. Do not reuse a consumed token. Inspect the conversation status before creating a replacement, and apply backoff for transient failures. See errors.
