Stream Message Layer - Sanas Developer Hub
Documentation Index
Fetch the complete documentation index at: /llms.txt
Use this file to discover all available pages before exploring further.
The Stream message layer defines the application protocol shared by the WebSocket Stream API and the WebRTC API. It covers session setup, pipeline configuration, turn finalization, transcription and translation events, lifecycle events, errors, and usage modes. Transport mapping:
| API | JSON messages | Audio |
|---|---|---|
| WebSocket Stream API | WebSocket text frames | Raw PCM bytes in WebSocket binary frames |
| WebRTC API | Stringified JSON on the WebRTC data channel | WebRTC audio media tracks |
Do not send audio in JSON messages. The init, configure, flush, and server event shapes are the same across transports.
Protocol Overview
Connection Lifecycle
ServerClientServerClientalt[Manual turn finalization][Automatic turn finalization]opt[Start another turn]opt[End session]
- Open authenticated Stream transport
- Send init with session parameters
- Send configure with languages and voice
- Send configured
- Stream input audio over transport
- Send transcription events
- Send translation events
- Stream output audio over transport
- Send output boundary and lifecycle events
- Send flush
- Detect end of speech with VAD
- Send remaining results and flushed
- Send new configure
- Close transport
Key Concepts
- Init: Sent once per connection. Sets session-level parameters (sample rates, conversation ID).
- Configure: Sent after init and again at any point to change languages, voice, or glossary. Each configure starts a new pipeline configuration. The server responds with
configuredwhen ready. - Flush: Signals the server to finalize the current input and produce all remaining output. The server responds with
flushedwhen complete. Use this for manual turn-taking or to force processing of buffered audio. - Utterance: A continuous segment of speech. The server assigns an incrementing
utterance_idxto each detected utterance. When VAD (Voice Activity Detection) is active, the server automatically splits speech into utterances.
Client Messages (Client -> Server)
Client control messages are JSON objects with a "type" field. Send them as WebSocket text frames on the WebSocket transport, or as stringified JSON messages on the WebRTC data channel. Audio transport is described in Input Audio.
init
Required as the first message on every connection. Sets connection-level parameters that remain fixed for the session lifetime.
{
"type": "init",
"conversation_id": "optional-id",
"session_name": "my-session",
"input_sample_rate": 16000,
"output_sample_rate": 16000,
"realtime_playback": true
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | Yes | — | Must be "init" |
conversation_id |
string | No | auto-generated UUID | Identifier to group related sessions (e.g., a multi-turn conversation). If empty or omitted, the server auto-generates a UUID. |
session_name |
string | No | "" |
Human-readable label for the session |
input_sample_rate |
integer | No | 16000 |
Sample rate of input audio in Hz. Allowed: 16000 |
output_sample_rate |
integer | No | 16000 |
Desired sample rate of output audio in Hz. Allowed: 16000 |
realtime_playback |
boolean | No | false |
Set to true if you intend to play output audio in real-time as it streams. This enables server-side pacing optimizations for simultaneous mode. Set to false for consecutive mode where the full translation audio is played after the speaker finishes. |
configure
Initializes or reconfigures the translation pipeline. Must be sent after init. Can be sent again mid-session to change languages, voice, glossary, or features without reconnecting.
{
"type": "configure",
"language_routes": [
{"lang_in": "en-US", "lang_out": "es-ES"}
],
"features": [],
"voice_id": null,
"glossary": [
{"terms": {"en-US": "Sanas", "es-ES": "Sanas"}}
],
"request_id": "optional-correlation-id"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | Yes | — | Must be "configure" |
language_routes |
array | No | [] |
Array of language route objects (see below) |
features |
array | No | [] |
Pipeline features to enable |
voice_id |
string or null | No | null |
Custom voice ID for speech synthesis. If null, uses a default voice for the target language. |
glossary |
array | No | [] |
Array of glossary entry objects (see below) |
request_id |
string or null | No | null |
Client-specified ID echoed back in the configured response for correlation |
The server responds with a configured message when the pipeline is ready to accept audio.
Language Routes
Language routes define the translation directions the pipeline supports. Each route specifies a source and target language.
{"lang_in": "en-US", "lang_out": "es-ES"}
| Field | Type | Description |
|---|---|---|
lang_in |
string | Source language code (BCP-47, e.g., "en-US", "es-ES", "fr-FR") |
lang_out |
string | Target language code |
Single route — translates one direction only:
"language_routes": [
{"lang_in": "en-US", "lang_out": "es-ES"}
]
Bidirectional routes — the server detects which language is being spoken and translates to the other. Use this when both parties may speak either language (replaces the old can_lang_swap behavior):
"language_routes": [
{"lang_in": "en-US", "lang_out": "es-ES"},
{"lang_in": "es-ES", "lang_out": "en-US"}
]
When multiple routes are provided, the server performs language identification on the incoming audio and selects the matching route. You will receive a language_route event indicating which route was chosen for each utterance. Auto-detect mode (any language to one target) — use "*" as lang_in to accept any supported language and translate it to a fixed target. The server identifies the spoken language automatically and translates to the specified output language:
"language_routes": [
{"lang_in": "*", "lang_out": "en-US"}
]
This translates any incoming language to English. The server will send a language_route event with the detected language filled in:
{"type": "language_route", "utterance_idx": 0, "lang_in": "ja-JP", "lang_out": "en-US", "is_final": true}
In auto-detect modes, the server may send tentative language_route messages (is_final: false) while language identification is still refining, followed by a single is_final: true message once the route is locked in. Clients that only care about the final answer can ignore tentative messages and act on is_final: true. You can combine auto-detect with an explicit route. For example, to translate English to Spanish and auto-detect everything else to English:
"language_routes": [
{"lang_in": "en-US", "lang_out": "es-ES"},
{"lang_in": "*", "lang_out": "en-US"}
]
NOTE: When multiple language routes are specified, they will be matched in list order. The first route to match wins. In the following language route configuration, the server will always choose the first route because * matches any detected language. In this case, the consequence is: English speech will remain as English.
"language_routes": [
{"lang_in": "*", "lang_out": "en-US"},
{"lang_in": "en-US", "lang_out": "es-ES"},
]
Wildcard catch-all route — add a "*" -> "*" route alongside your normal routes to handle speech in unexpected languages gracefully. When the detected language does not match any explicit route, the wildcard route is selected and the pipeline enters a transcription-only mode (no translation or TTS output). This is useful for prompting the user to switch languages:
"language_routes": [
{"lang_in": "en-US", "lang_out": "es-ES"},
{"lang_in": "es-ES", "lang_out": "en-US"},
{"lang_in": "*", "lang_out": "*"}
]
When the wildcard catch-all route is triggered, you will receive a language_route event with the detected language echoed into bothlang_in and lang_out. For the configuration above, if the user speaks French you will see:
{"type": "language_route", "utterance_idx": 1, "lang_in": "fr-FR", "lang_out": "fr-FR", "is_final": true}
Note that the server then emits transcription / transcription_ended for the utterance but no translation, translation_ended, output_audio, output_speech_started, or output_speech_ended (transcription-only mode). You can use this language_route to show a prompt to the user, for example: “It seems like you are speaking French. Would you like to switch your language?” If the user confirms, send a new configure with the appropriate routes.
Glossary Entries
Glossary entries either (a) keep a phrase verbatim across languages or (b) lock in a specific translation between two languages. Each entry is a map of language codes to the desired term in that language. Keep a phrase verbatim across languages — use "*". For brand names, product names, and other proper nouns that should never be translated, use the "*" wildcard:
{"glossary": [
{"terms": {"*": "Sanas"}},
{"terms": {"*": "ACME"}}
]}
This tells the pipeline that “Sanas” should remain “Sanas” in every language, and “ACME” should remain “ACME” in every language. Lock in a specific translation between 2+ languages — use language codes. To force a phrase in one language to a specific phrase in another, list each phrase by its language code:
{
"terms": {
"es": "servicio al cliente",
"fr": "service client",
"en": "customer service"
}
}
Here, “servicio al cliente” spoken in Spanish becomes “service client” when translating to French, and “service client” spoken in French becomes “servicio al cliente” when translating to Spanish. A glossary entry only takes effect on a language route when both the input and output languages are specified. If either side isn’t covered, the entry is ignored for that route.
Features
The features array controls which pipeline mode is used. By default, you can leave the features array empty or omitted. Note that feature names are case-sensitive:
| Feature | Description |
|---|---|
"consecutive" |
By default, simultaneous mode is used, producing streaming translation and audio output in real-time as the speaker talks. This feature enables consecutive translation mode. The pipeline waits for the speaker to finish (via VAD or manual flush), then produces the full translation and synthesized audio. |
"detect_languages_once" |
By default, when language_routes actually require detection (any "*" lang_in, or multiple routes), every utterance redetects the language. This feature locks the session to only detect language a single time. |
"hide_output_text_boundary" |
Suppresses output_text_boundary emission. By default the server aligns TTS word timestamps back to the transcription/translation and emits boundary messages, suitable for word-level highlighting. Set this feature to skip all alignment work. |
"skip_logging" |
If not already, opts this session out of server-side recording. Once set on any configure in the session, it sticks for the lifetime of the stream. |
Input Audio
Streams audio data to the server after receiving configured. Audio transport depends on the API you are using:
| Transport | Input audio behavior |
|---|---|
| WebSocket Stream API | Send raw PCM bytes as Binary WebSocket frames. Do not wrap audio in JSON. |
| WebRTC API | Send microphone audio on the WebRTC audio media track. Do not send audio on the data channel. |
For WebSocket binary audio, use this format:
- Encoding: 16-bit signed integer PCM (little-endian)
- Channels: Mono (1 channel)
- Sample rate: Must match
input_sample_ratefrominit
flush
Signals the server to finalize processing of all buffered audio and produce remaining results. The server will respond with a flushed message after all output has been sent.
{
"type": "flush",
"request_id": "optional-correlation-id"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | Yes | — | Must be "flush" |
request_id |
string or null | No | null |
Client-specified ID echoed back in the flushed response for correlation |
Server Messages (Server -> Client)
Server control and text messages are JSON objects with a "type" field. Receive them as WebSocket text frames on the WebSocket transport, or as stringified JSON messages on the WebRTC data channel. Audio transport is described in Output Audio. Note on optional fields: Fields documented as “or null” (such as request_id, probability, utterance_idx on errors) are omitted from the JSON when not present, rather than sent as "field": null. Your client should treat a missing key the same as a null value.
configured
Confirms the pipeline is ready to accept audio after a configure message.
{
"type": "configured",
"request_id": "r1"
}
| Field | Type | Description |
|---|---|---|
type |
string | "configured" |
request_id |
string or null | Echoes the request_id from the corresponding configure message, if provided |
input_speech_started
Indicates the server’s VAD has detected the start of speech in the input audio.
{
"type": "input_speech_started",
"utterance_idx": 0,
"input_time": 0.5
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance that started |
input_time |
float | Timestamp in seconds (relative to the start of the input audio stream) when speech was detected |
language_route
Indicates which language route was selected for the current utterance. Sent when the system identifies the spoken language, particularly useful when multiple language routes are configured.
{
"type": "language_route",
"utterance_idx": 0,
"lang_in": "en-US",
"lang_out": "es-ES",
"is_final": true
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance this route applies to |
lang_in |
string | Detected/confirmed input language |
lang_out |
string | Target output language |
is_final |
boolean | true when this is the locked-in route for the utterance |
transcription
Real-time transcription of the input audio. Complete words have been finalized and will not change. Partial words are in-progress and may be updated in subsequent messages.
{
"type": "transcription",
"utterance_idx": 0,
"complete": [
{"word": "Hello ", "start": 0.0, "end": 0.5, "probability": 0.98}
],
"partial": [
{"word": "world", "start": 0.5, "end": 0.8, "probability": 0.85}
]
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance this transcription belongs to |
complete |
Word[] | Finalized words — will not change and are sent only once. Store these on the client for display. |
partial |
Word[] | In-progress words — may be updated or replaced in subsequent messages |
Word object:
| Field | Type | Description |
|---|---|---|
word |
string | The transcribed word (may include trailing whitespace) |
start |
float | Start time in seconds relative to the beginning of the utterance’s input audio |
end |
float | End time in seconds |
probability |
float or null | Confidence score (0.0 to 1.0), or null if unavailable |
transcription_ended
Indicates that all transcription for a given utterance is complete. No further transcription messages will be sent for this utterance.
{
"type": "transcription_ended",
"utterance_idx": 0
}
translation
Translated text derived from the transcription. Like transcription, includes complete (finalized) and partial (in-progress) words.
{
"type": "translation",
"utterance_idx": 0,
"complete": [
{"word": "Hola ", "start": 0.0, "end": 0.5, "probability": 0.95}
],
"partial": [
{"word": "mundo", "start": 0.5, "end": 0.8, "probability": 0.85}
]
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance this translation belongs to |
complete |
Word[] | Finalized translated words |
partial |
Word[] | In-progress translated words |
translation_ended
Indicates that all translation for a given utterance is complete.
{
"type": "translation_ended",
"utterance_idx": 0
}
input_speech_ended
Indicates the server’s VAD has detected the end of speech in the input audio. After this, the server will not accept further input audio for this utterance and will finalize the translation.
{
"type": "input_speech_ended",
"utterance_idx": 0,
"input_time": 3.2
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance that ended |
input_time |
float | Timestamp in seconds when end-of-speech was detected |
output_speech_started
Indicates the server has begun sending synthesized output audio for an utterance.
{
"type": "output_speech_started",
"utterance_idx": 0,
"output_time": 0.0
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance |
output_time |
float | Timestamp in the output audio stream (seconds) |
Output Audio
Synthesized audio from the translated text. Audio transport depends on the API you are using:
| Transport | Output audio behavior |
|---|---|
| WebSocket Stream API | Receive raw PCM bytes as Binary WebSocket frames. Audio is not wrapped in JSON. |
| WebRTC API | Receive translated audio on the WebRTC audio media track. Audio is not sent on the data channel. |
For WebSocket binary audio, the format is:
- Encoding: 16-bit signed integer PCM (little-endian)
- Channels: Mono (1 channel)
- Sample rate: Matches
output_sample_ratespecified ininit
output_text_boundary
Timing information that maps a position in the output audio stream back to positions in the transcription and translation text. Use this to synchronize text highlighting with audio playback. Sent by default. Clients that don’t need word-level highlighting can opt out by setting the "hide_output_text_boundary" feature in configure, which suppresses these messages.
{
"type": "output_text_boundary",
"utterance_idx": 0,
"output_time": 1.5,
"transcription": {"word_idx": 3, "char_idx": 15},
"translation": {"word_idx": 2, "char_idx": 12}
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance |
output_time |
float | Timestamp in seconds within the output audio stream |
transcription |
TextPosition or null | Position in the transcription text that aligns with the output audio at output_time |
translation |
TextPosition or null | Position in the translation text that aligns with the output audio at output_time. All translation before this position has already been spoken in the output audio. |
TextPosition object:
| Field | Type | Description |
|---|---|---|
word_idx |
integer | Index of the word within the utterance’s word list |
char_idx |
integer | Character offset within the word |
output_speech_ended
Indicates all synthesized audio for an utterance has been sent.
{
"type": "output_speech_ended",
"utterance_idx": 0,
"output_time": 4.2
}
| Field | Type | Description |
|---|---|---|
utterance_idx |
integer | Index of the utterance |
output_time |
float | Final timestamp of the output audio |
flushed
Confirms that a flush has been fully processed and all pending output has been sent.
{
"type": "flushed",
"request_id": "f1"
}
| Field | Type | Description |
|---|---|---|
request_id |
string or null | Echoes the request_id from the flush message, if provided |
error
Indicates an error occurred during processing.
{
"type": "error",
"message": "Invalid config message",
"code": "CORE_ERROR",
"utterance_idx": 3,
"request_id": "r1"
}
| Field | Type | Description |
|---|---|---|
message |
string | Human-readable error description |
code |
string | Error code identifier (e.g., "STREAM_ERROR" from the proxy, or an application-specific code from the backend) |
utterance_idx |
integer or null | Utterance associated with the error, if applicable |
request_id |
string or null | Associated request ID, if applicable |
Non-fatal errors (e.g., malformed JSON, unknown message type) are sent as error messages with code "STREAM_ERROR" but the connection remains open. You can continue sending messages after receiving an error. Errors originating from the backend translation pipeline will have their own error codes.