Skip to main content

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

Single-phase finals. Each turn produces one committed 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.
Do not send the start message until connection_established is received. The backend proxy attaches its client-message listener only after authenticating and opening the upstream connection. Messages sent earlier will be silently dropped. Likewise, do not send audio messages until session_started is received — the upstream returns unknown message type: audio if it arrives before the session is ready.

Client → Server Messages

start — Begin session

Sent once after connection is established. Must be sent before any audio.
Parameters:
Auto-detect: omit languages, or send null, [], "auto" or ["auto"]. The /ws/stt proxy expands all of these to the full language list the STT server advertises (session_started.language_mode is then candidate_auto; a single code gives selected). The STT server itself rejects the literal "auto" and fails real speech on an empty list, so the proxy does the expansion for you.

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)

Fields:

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

Send only what changes. Each recognised key is acknowledged with its own event: 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

Closes the current turn immediately and returns its 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

Server processes any remaining audio buffer, sends session_stopped, then closes.

test — Ping / latency check

Server echoes test_response with the same timestamp for round-trip measurement.

Server → Client Messages

connecting — Authentication in progress

connection_established — Authentication successful

On STT these fields are top-level and the frame is identified by type. Branch on msg.type === "connection_established" — not on msg.connection_established, which is the TTS frame’s shape (/ws/tts nests the same fields under a connection_established key). A client written against the TTS shape will silently never start its STT session.
Fields:
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.
Trust this event over your request — it echoes the effective settings. Fields worth branching on:

speech_started — VAD detected voice activity

Use this for barge-in: interrupt TTS playback when this arrives. Fired after 2 consecutive VAD-positive chunks (~64ms of confirmed speech).

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 (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.
Interim result (is_final=false, speech_final=false) — only sent when interim_results_frequency is set:
Interims are live guesses for display and hints — the 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 no text field. 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

Sent once when binary audio arrives without 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 terminal field) — the server rejected a client message (bad start, invalid config, unknown type, mismatched audio format). 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 terminal no_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 code 1008:
Existing sessions are unaffected; the cap releases when an in-flight session ends. Do not auto-reconnect on 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 including ja 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 send start 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 / 1000 bytes)
    • Maximum utterance duration: 30 s (anything longer force-finalizes)
    VAD
    • Speech start: Silero probability > STT_VAD_THRESHOLD (default 0.5, server-configurable)
    • Silence → utterance end: utterance_end_ms of consecutive sub-threshold audio
    • No separate continuation threshold
    Non-hallucinating architecture The 60db STT backend uses non-hallucinating architectures that do not output 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

    Then send: