STT WebSocket
Real-time Speech-to-Text transcription via WebSocket streaming with support for 45 languages (including code-switched Indic+English) and telephony integration. Powered by 60db STT v01 (a non-hallucinating, multi-backend speech recognition stack).Endpoint
Authentication
Query parameter authentication: Examples:The WebSocket connection checks workspace wallet balance before starting a session. If the workspace has insufficient credits, the connection is closed with a
1008 status code and an INSUFFICIENT_CREDITS error.Connection Details
The same port accepts plain HTTP GET requests and responds
200 ok — safe for load-balancer health checks.
Session Lifecycle
transcription with is_final: true (and speech_final mirrors is_final — they are always equal). Text is never rewritten after recognition, so there is no second “canonical” emit and no refined event. Interims (is_final: false) are optional live guesses; only finals are committed. A single caller turn can still yield more than one final — a pause can split it, and a monologue longer than max_utterance_sec emits one or more segment_final: true chunks before the closing final. Each final covers a non-overlapping slice of audio, so append them.
Client → Server Messages
start — Begin session
Sent once after connection is established. Must be sent before any audio.
Turn-ending profiles
early_final and turn_detector together decide how fast a turn closes and whether a sentence with a pause stays in one final. Pick one profile:
No words are lost with any profile — only where turns are cut and how fast finals arrive changes.
session_started echoes the effective values.
audio — JSON audio chunk (browser mode)
Binary frame — raw μ-law audio (telephony mode)
Send a raw WebSocket binary frame with μ-law bytes, no JSON wrapper. The server auto-detects this as telephony mode on the first binary frame.config — Change settings mid-session
If any value is invalid you get an
error and nothing in that message is applied. Changing languages mid-sentence first finalizes that sentence, then switches — prefer switching between turns. diarize, min_speakers, and max_speakers can only be set in start. Send "languages": null to revert to auto-detect. The proxy applies the same sanitising as on start: "auto" is stripped, and utterance_end_ms is snapped onto the ≥1000 / step-100 grid.
finalize — End the current turn now
transcription (or no_speech), or nothing if no audio is buffered. The session continues. Use it when your application already knows the caller is done — e.g. after a DTMF key.
stop — End session
session_stopped, then closes.
test — Ping / latency check
test_response with the same timestamp for round-trip measurement.
Server → Client Messages
connecting — Authentication in progress
connection_established — Authentication successful
string
Service name:
"stt"integer
Your user ID
number
Available credits
string
Workspace name
connected — Proxy wired to upstream STT server
Sent by the backend proxy after it has opened the upstream connection and attached its client-message listener. After this point, client messages are no longer dropped.
You will normally see two
connected frames, and they carry different payloads. The proxy emits its own handshake frame first (session_id plus a small server_info), then forwards the upstream server’s capability frame (the one below, with features and timing). Both are informational — treat connection_established as “safe to send start” and session_started as “safe to send audio”, and make any connected handler idempotent so it does not start audio capture twice.server_info on the proxy frame is diagnostic; do not branch on its contents.session_started — start message processed, audio is now accepted
Sent by the upstream STT server after a start message is received and validated. It is safe to begin sending audio frames immediately after this event.
speech_started — VAD detected voice activity
transcription — Transcription result
All results (interim and final) share the same transcription type — differentiate with flags.
Final result (is_final=true, speech_final=true):
segment_final=true) — max_utterance_sec cut a long monologue; the same turn continues with more finals to come. Check segment_final before speech_final.
Withheld final (text="", processing_mode="low_confidence_transcript") — the text was too short and too unsure to trust. Only sent when all hold: confidence below 0.55, at most 4 words, at most 14 characters. The event still arrives so you can clear the interim it replaces; it is not billed.
is_final=false, speech_final=false) — only sent when interim_results_frequency is set:
text is the guess for the whole turn so far, so replace the displayed line with each interim (never append). Never send interim text to your LLM or business logic — use finals only. A final always follows.
Response Fields:
string
Transcribed text. Empty string = speech-end-no-result signal.
number
0.0–1.0. Telephony typically 0.35–0.75; browser 0.55–0.95.
string
Detected language code e.g.
"en".string
Uppercase language code (e.g.
"EN") in the WS shape. Note: REST /stt returns the full English name ("English") here — WS preserves the legacy uppercase-code shape for client compatibility.boolean
false = interim · true = committed final for the turn.boolean
Same as
is_final — the two flags always carry the same value.boolean
true = this final was cut by the max_utterance_sec length cap and more of the same turn follows. Check this before speech_final.boolean
true for interim results only.integer
Turn number. Interims carry the id their final will have.
sentence_id is the same value.integer
Same value as
utterance_id.string
Unique id of a committed result (finals only), e.g.
"aa65…:3". Use it to de-duplicate — if a network retry delivers a final twice, drop the duplicate.string
What ended the turn:
vad_silence, client_finalize, client_stop, max_utterance, language_change, or response_timeout. interim on interims.number
Duration (seconds) of the audio segment transcribed.
number
Seconds from processing start to result ready (excludes queue time).
array
Word-level timestamps
[{word, start, end, probability}], seconds from turn start. Finals only; empty on interims. For Indian languages the times are evenly-spread estimates and probability repeats confidence.When the keyword/context-terms boost replaced a word, the entry may include
boosted: true and original (the pre-boost word). The segment-level text
is already rebuilt from boosted words upstream — no client-side stitching
required.array
List of
[{speaker, start, end}] diarization turns when config.diarize=true. Omitted or null otherwise. Raw speaker IDs look like SPEAKER_00, SPEAKER_01; clients typically re-label these as “Speaker 1”, “Speaker 2” in order of first appearance.string
How the result was produced. On ordinary finals:
sentence_complete; on a
max_utterance_sec cut: max_utterance_segment; on interims:
streaming_interim. A withheld final carries low_confidence_transcript
(empty text, not billed). Treat any unrecognised value as “withheld”.number
Audio signal-to-noise ratio (dB) for this utterance, when measured.
Optional. Surface as a “good / fair / poor” badge:
>= 15 good, 0–15
fair, < 0 poor.no_speech — The turn had no transcribable audio
terminal: true— the turn is over with no words. Clear the interim display; if segment finals were collected for this turn, dispatch them now.terminal: false— an interim check found no words; the turn is still open. Ignore it (or just clear the interim). There is notextfield. Not billed.
turn_complete — Turn detector confirmed end of thought
Only with turn_detector: true. Sent right before the final it confirms.
forced: true means the turn was ended by the safety limit (3 × utterance_end_ms) even though the model judged it unfinished.
interim_delta — Append/replace hint for interims
Only with interim_delta_streaming: true. Sent alongside each interim.
operation: "append" → add text to the line. operation: "replace" → the guess changed; replace the line with full_text.
transcription_tokens — Stable word tokens for animation
Only with token_streaming: true. Word tokens with stable ids for UIs that animate words. transcription stays the source of truth.
event: append for interims, finalize for the final.
language_changed — After config message changes language
Acknowledgements — After a config change
Each recognised config key is acknowledged with its own event (utterance_end_changed, turn_detector_changed, interim_frequency_changed, interim_delta_streaming_changed, token_streaming_changed, max_utterance_changed, audio_enhancement_changed, audio_format_changed, keywords_changed, context_changed). See the config table for the full mapping.
warning — Non-fatal notice
encoding/sample_rate declared in start. Informational — the session continues.
session_stopped — After stop is processed
billing_summary fields:
What gets billed: every committed final (
is_final: true) that carries a non-empty text and a duration. Each final covers a non-overlapping slice of audio, so segment finals and short-pause splits sum to the real spoken time with no double-charge. Withheld finals (text: "", processing_mode: "low_confidence_transcript"), no_speech, and interims are never charged — silence, noise and unusable audio cost nothing.
total_cost here reflects only the STT usage; session_stopped also repeats the last final’s text/transcript on the upstream frame, but you should build the full call transcript from the finals you received, not from this event.error — Rejected message or failed turn
There are two kinds of error frame, told apart by terminal:
- Non-terminal (no
terminalfield) — the server rejected a client message (badstart, invalidconfig, unknowntype, mismatchedaudioformat). The turn and session are unaffected; fix and resend. - Terminal (
terminal: true) — recognition of a turn failed (overload, timeout, internal error). That turn is closed without text but the session stays open. Handle it like a terminalno_speech: clear the interim, dispatch any finals already collected, keep the session. If it repeats, reprompt or reconnect.
Concurrency-limit error frame
When a user has reached their per-user STT session cap (counted across REST + WS combined), the server sends an error frame and closes with code1008:
1008 — the limit only frees when an in-flight session completes. The frontend should distinguish concurrency-limit closes from auth/network failures so the UI message is correct.
connection_closed — Upstream dropped the session
Sent when the upstream STT service closes its side. Billing is finalised at this point and the client socket is closed straight after, so treat it as terminal and reconnect if you still have audio to send.
test_response — Reply to test ping
Complete Example
Audio Requirements
Supported Languages
45 transcription languages total, including 12 Indic languages with Hinglish code-switching, Arabic (MSA), and a broad European/East-Asian set (now includingja Japanese, ko Korean, zh Mandarin, vi Vietnamese, tr Turkish, and he Hebrew). GET /v1/languages (or GET /stt/languages) is the authoritative catalog, including each language’s quality_tier.
Indian (12): as Assamese, bn Bengali, gu Gujarati, hi Hindi, kn Kannada, ml Malayalam, mr Marathi, ne Nepali, or Odia, pa Punjabi, ta Tamil, te Telugu.
Other (33): ar Arabic, bg Bulgarian, cs Czech, da Danish, de German, el Greek, en English, es Spanish, et Estonian, fi Finnish, fr French, he Hebrew, hr Croatian, hu Hungarian, it Italian, ja Japanese, ko Korean, lt Lithuanian, lv Latvian, mt Maltese, nb Norwegian Bokmål, nl Dutch, pl Polish, pt Portuguese, ro Romanian, ru Russian, sk Slovak, sl Slovenian, sv Swedish, tr Turkish, uk Ukrainian, vi Vietnamese, zh Mandarin.
Code-switching (Indic + English): Mixed speech such as Hinglish is handled within a turn — Hindi in Devanagari, English words in Latin. en+hi is the most-tested pair.
Arabic dialect tags (ar-eg, ar-lv, …) are rejected — pass ar for best-effort MSA.
Limitations and Best Practices
Handshake ordering (most common mistake) Do not sendstart in ws.onopen. Wait for the proxy’s connection_established message first — it marks the point at which the proxy has attached its client-message listener. Likewise, wait for session_started before sending audio frames, otherwise the upstream server returns unknown message type: audio.
utterance_end_ms floor and grid
The minimum is 1000 ms, in steps of 100. The upstream server rejects (does not adjust) a value below 1000 or off the grid, so the 60db proxy snaps the value onto the nearest valid step and floors it at 1000 ms before forwarding — an older client is silently corrected instead of disconnected. If you need faster turn-taking, use profile C (early_final: true) and drive barge-in from speech_started and interims, not from this value.
Real-time pacing
This endpoint is for live audio. Send frames in real time (20–100 ms each); bursts / faster-than-real-time audio can be rejected and may close the socket with code 1013. Use the REST /stt endpoint for pre-recorded files. Keep sending during silence — a socket with no frames for 50 s is closed.
One session per caller
Do not multiplex several callers on one socket. There is no echo cancellation at this layer — if bot audio leaks into the caller’s channel it may be transcribed as the caller, so cancel echo before sending audio.
Language count
Max 5 languages per session. Cross-backend multi-language (e.g. ["en","ar"]) runs per-utterance LID which adds ~20–50 ms per utterance. Same-backend multi-language (e.g. ["en","hi"]) collapses to a single backend’s fast path with zero LID overhead — use this whenever possible.
Telephony confidence
8 kHz μ-law → 16 kHz resampling reduces backend confidence by ~0.10–0.15 compared to native wideband input. Use a client-side threshold of 0.35 for telephony vs 0.55 for browser.
Buffer limits
- Pre-speech ring buffer: 1.0 s (captures first word before VAD fires)
- Minimum coalesce before VAD: 160 ms (μ-law 1280 bytes, linear
sample_rate × 2 × 160 / 1000bytes) - Maximum utterance duration: 30 s (anything longer force-finalizes)
- Speech start: Silero probability >
STT_VAD_THRESHOLD(default 0.5, server-configurable) - Silence → utterance end:
utterance_end_msof consecutive sub-threshold audio - No separate continuation threshold
no_speech_prob — the CTC/RNN-T topology emits blank tokens on non-speech, surfaced as no_speech. Text is never rewritten after recognition (no LLM refinement); keywords/context.terms only correct near-miss spellings of the terms you supply.
Diarization
diarize=true requires HF_TOKEN on the server plus gated model approval for pyannote/speaker-diarization-3.1. Without both, the request silently falls back to a deterministic mock. Check session_started.diarize to confirm the request was accepted.
Pricing
- Rate: $0.00000833 per second
- Minimum: $0.01 per session
- Billing: Per second of audio processed
Error Codes
A terminal
error frame (terminal: true, with a failure_type — see error) means one turn failed but the session stays open; it is not a close code.
error_code sits alongside error on the frame — for example {"type":"error","error":"Insufficient credits to start STT session","error_code":"INSUFFICIENT_CREDITS","details":{"required":0.01,"available":0}}. One exception: when the wallet runs out mid-session, the deduction failure is reported as {"type":"error","error":{"code":"INSUFFICIENT_CREDITS","message":"…"}} — a nested object — before the socket closes with 1008. Handle both shapes if you parse credit errors.
Testing
Related
- TTS WebSocket - Text-to-Speech endpoint
- WebSocket API Reference - Complete documentation
- Voices API - Get available voices