Skip to main content
Sayna’s /ws endpoint is the control plane for every real-time experience. A single socket handles streaming audio, transcription events, TTS playback, and LiveKit data. This page distills the full protocol from the engineering brief (tmp/docs/websocket.mdoc) into actionable reference material.

End-to-end flow

1

Handshake

Open a WS connection (ws://host:3001/ws or wss://). Ping/pong is automatic; idle sockets close after ~10 s of silence.
2

Config

The very first client message must be type: "config". Sayna injects credentials, boots STT/TTS providers, provisions LiveKit (if configured), warms caches, and responds with ready once every subsystem is up (30 s timeout).
3

Active session

Stream binary PCM/Opus frames, queue speak jobs, drive the loading indicator with loading_start / loading_stop, forward LiveKit data via send_message, and react to stt_result, message, participant_disconnected, and binary TTS audio frames.
4

Cleanup

On close (client, timeout, or LiveKit departure) Sayna halts providers, stops recordings, leaves/deletes the room, flushes caches, and releases file handles.

Message taxonomy

Configuration message

  • audio=false skips STT/TTS initialization but still allows LiveKit control + data channel relays.
  • stream_id is optional; provide one to control recording paths and correlate sessions, or let Sayna generate a UUID v4 and return it in the ready message.
  • stt_config.sample_rate/channels/encoding must exactly match the binary frames you send—Sayna will not resample.
  • tts_config values become session defaults; individual speak messages can override most fields or add id for correlation.
  • stt_config.auth and tts_config.auth are optional objects that supply provider credentials for this session only. When provided, they fully replace server-configured credentials (no partial merge). Omit or send {} to use server defaults. See Provider auth overrides below for supported shapes per provider.
  • The livekit block is optional but required for send_message and mirrored playback/recording.
  • loading_audio is optional and additive; it is only processed when audio=true and the livekit block is present, and a decode failure is non-fatal (the session continues without loading capability). See the dedicated Loading indicator section below.

Streaming audio best practices

  • Chunk size: ~100–200 ms of audio per binary frame (e.g., 3 200 bytes for 16 kHz mono linear16).
  • Maintain consistent cadence; bursty uploads increase STT latency.
  • Enable punctuation + turn-detect (default features) when you need is_speech_final cues to know when a speaker stopped.
  • If the client detects a barge-in (human interrupts AI), send {"type":"clear"} before queueing new speak text.

Client commands in detail

speak

  • flush=true clears queued audio before speaking; set false to build playlists.
  • allow_interruption=false protects critical prompts even if the client sends clear.
  • tts_config may override any session default, including provider credentials via the auth field.
  • Finish signal arrives via tts_playback_complete with the same id.

loading_start

Begins the loading-indicator audio loop on the dedicated "loading-audio" LiveKit track. Fire-and-forget; idempotent if the loop is already running. Requires audio=true, an active LiveKit room, and a loading_audio clip supplied in the initial config message. Failures (no config yet, audio disabled, no LiveKit room, missing or undecodable loading_audio, track publish failure) surface as error messages on the existing error channel.

loading_stop

Stops the loading loop with a short fade-out (~30 ms). Fire-and-forget, idempotent, and always silent server-side — stopping when nothing is playing or when no LiveKit room exists is a harmless no-op, never an error. Because speak does not stop the loop, applications must send loading_stop before (or together with) speak in the normal “thinking finished, now answer” flow. Otherwise the loading sound continues playing under the spoken answer.

send_message

Used when Sayna should notify LiveKit participants via data channels (chat bubbles, control events). Requires a livekit block in the configuration message.

Server events in detail

ready

Echoes the session stream_id (provided or auto-generated), LiveKit metadata (room name, URLs, participant identity), and signals that STT/TTS providers finished booting. Use that stream_id later with GET /recording/{stream_id} when recording is enabled. Other participants still need to call POST /livekit/token to join the room.

stt_result

  • Interim updates set is_final=false and keep streaming as long as audio flows.
  • The final chunk for an utterance flips is_final=true.
  • is_speech_final=true indicates the turn detector heard silence—ideal for triggering a response.

message and participant_disconnected

Utility events for LiveKit mirroring:
When a participant_disconnected event arrives, expect Sayna to close the WebSocket 100 ms later so the LiveKit room is torn down cleanly.

Binary TTS payloads

Binary frames deliver synthesized audio in the format you declared (e.g., 24 kHz linear16). Handle them alongside JSON control frames:

Provider auth overrides

Both stt_config and tts_config accept an optional auth object that lets clients supply provider credentials per WebSocket session or per POST /speak request. This is useful for multi-tenant deployments, customer-managed provider keys, and per-request credential routing.

Fallback behavior

  • If auth is omitted, the server uses its configured provider credentials.
  • If auth is sent as an empty object {}, the server also falls back to its configured credentials.

No partial merge

If auth is provided, it must be complete for that provider. The server does not merge a partial auth object with server-side credentials. For example, sending Azure auth with only api_key and no region is invalid.

Supported auth shapes

Validation

Clients should expect session or request initialization to fail if:
  • Required auth fields for the selected provider are missing.
  • Auth field types are invalid.
  • Unsupported auth keys are sent for that provider.
  • The provider name is unsupported.
The provider value in stt_config.provider or tts_config.provider determines how the auth object is interpreted — the auth object itself does not include a provider discriminator.

Loading indicator

The loading indicator is a short audio clip looped into the LiveKit room while your application is busy (“thinking”) and cannot yet answer. It is the audio equivalent of a spinner. The clip plays on its own dedicated LiveKit audio track named "loading-audio", separate from the "tts-audio" speech track, so it never interferes with text-to-speech output.

loading_audio configuration

Include a loading_audio object in the initial config message. The server decodes and validates the clip once at config time and holds the validated buffer for the session. Audio requirements
  • Only 16-bit PCM is supported (16-bit integer WAV, or raw 16-bit little-endian PCM). Other bit depths are rejected.
  • Decoded duration: ~250 ms to ~10 s, 1–4 s recommended.
  • Decoded size is capped at ~1.9 MB.
  • Author the clip so its start and end amplitudes match — otherwise the loop seam will click.
  • Author the clip quiet enough to sit under speech rather than compete with it.
Behavioral rules
  • loading_audio is processed only when audio=true and a livekit block is present in the same config message. Supplied without those, the server emits an error and continues the session without loading capability.
  • Decode or validation failures are non-fatal — the session continues, STT/TTS are unaffected, and only the loading feature is unavailable. A later loading_start re-reports the original decode error so the failure is never silent.

Controlling the loop

The loop is controlled exclusively by the loading_start and loading_stop commands. speak and clear do not affect it. Server-side, the loop also stops automatically on session teardown / disconnect. The server does not stop the loop when VAD detects user speech. If you want the loading audio to end on barge-in, send loading_stop from your vad_event or stt_result handler.

Typical turn

LiveKit interplay

  • Sayna manages the agent-side participant automatically; humans still fetch tokens via POST /livekit/token.
  • listen_participants limits which identities can feed audio back into Sayna.
  • Recordings respect your LiveKit/S3 configuration when enable_recording=true; files are stored at {server_prefix}/{stream_id}/audio.ogg using the stream_id from your config or the ready message.
  • send_message and clear affect both the WebSocket client and LiveKit room so playback remains in sync.

Troubleshooting

Need more detail on SIP routing or carrier setup? Pair this guide with the SIP configuration and Twilio SIP setup references.