Skip to main content

STT WebSocket API

Real-time Speech-to-Text transcription via WebSocket streaming with support for 39 languages (including code-switched Indic+English) and telephony integration. Powered by 60db STT v01 (a non-hallucinating, multi-backend speech recognition stack).

🚀 Quick Start (Copy & Paste)

That’s it! You’ll see:
  • ✅ Authenticated
  • ✅ Ready! Send audio now
  • 📝 Hello world (transcribed text)
  • ✅ Complete! Cost: $0.000043

📖 How It Works (5 Simple Steps)

  1. Connect with your API key
  2. Send { type: "start", ... } to begin session
  3. Stream audio data (binary chunks)
  4. Receive text transcriptions in real-time
  5. Stop with { type: "stop" } when done

Endpoint

Authentication

Query parameter authentication: Example:

Connection Details

Session Lifecycle

One committed final per stretch of audio. Every result is a transcription with is_final: true, and speech_final mirrors is_final (there is no separate “canonical” phase and no LLM rewriting — see §Text is never rewritten). A single caller turn can still produce more than one final:
  • a short pause can split a sentence into two finals (with early_final: true);
  • a monologue longer than max_utterance_sec emits one or more segment_final: true chunks, then a normal final.
Each final covers a non-overlapping slice of audio, so append them in arrival order. De-duplicate on final_id. A turn that contained no words ends with a terminal no_speech instead.

Text is never rewritten

After recognition, the server never rewrites the script or re-guesses the language, and there is no LLM refinement pass. The only post-recognition change is strict keyword/term spelling correction from keywords / context.terms (force tuner → Fortuner). keywords correct near-miss spellings in place; word timings are preserved. Everything else in text is exactly what the recognizer produced.

Client → Server Messages

start — Begin session

Sent once after connection is established. Must be sent before any audio.
Parameters:

Turn-ending profiles

early_final and turn_detector together decide how fast a turn closes and whether a sentence with a mid-pause stays in one final. Pick one profile: Profile A is best for voicebots where a cut-off sentence causes a wrong answer. Profiles B and C are faster; join split finals in your middle layer if you use them. No words are lost with any profile — only where turns are cut changes. Confirm the effective values in session_started.

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 the fields you want to change. Send "languages": null to revert to auto-detect. Most changes are acknowledged with a *_changed event: language_changed, turn_detector_changed, utterance_end_changed, interim_frequency_changed, interim_delta_streaming_changed, token_streaming_changed, max_utterance_changed, audio_enhancement_changed, audio_format_changed, keywords_changed, context_changed. If any value is invalid you get an error and nothing in that message is applied. diarize, min_speakers and max_speakers can only be set in start. Changing languages mid-sentence first finalizes the current sentence, then switches — prefer switching between turns.

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 press.

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 tagged by type. Check msg.type === "connection_established" — msg.connection_established is the TTS frame’s shape, and a client written against it will never start its STT session.
Fields:
string
Service name: "stt"
integer
Your user ID
number
Available credits
string
Workspace name

connected — After start message is processed

Expect two connected frames: the proxy’s own handshake (session_id plus a small server_info), then the upstream capability frame shown above. Neither means “audio is accepted” — wait for session_started. Keep any connected handler idempotent so audio capture does not start twice.

session_started — start accepted, audio is now allowed

Trust this event over your request. It echoes the effective settings after normalization — check early_final/turn_detector here to confirm which profile the session is running. interim_frequency is the effective interim_results_frequency (null = off); interim_min_voiced_ms: null means the default (600). ignored_config_keys lists every key the server did not recognise (e.g. a browser echoCancellation) — unknown keys never fail a session. Send audio only after this event.

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, more finals follow:
Withheld final (text="", processing_mode="low_confidence_transcript"): Sent only when the text was too short and too unsure to trust — 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. Longer or confident text is never withheld, and a continuation after a short-pause split is never withheld.
Interim result (is_final=false, speech_final=false) — only sent when interim_results_frequency is set:
Use interims only for barge-in word-count checks. Never send interim text to the LLM — a final with is_final=true, speech_final=true will follow. 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".
boolean
false = interim · true = committed final for this stretch of audio.
boolean
Same as is_final. The two flags always agree — there is no separate “canonical” phase.
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 so a segment isn’t treated as the end of the turn.
boolean
true for interim results only.
integer
Turn number. Interims carry the id their final will have. sentence_id is the same value.
string
Unique id of a committed result. Use it to de-duplicate — if a network retry delivers one 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
Seconds of audio covered by this final. (utterance_end_ms is the same value in milliseconds.)
number
Server processing time in seconds.
array
Finals only: [{ word, start, end, probability }], seconds from turn start. The score field is probability. Empty on interims. For Indian languages the times are evenly spread estimates and probability repeats confidence.
integer
duration in milliseconds.
string
How this result was produced. On ordinary results: streaming_interim (interim), sentence_complete (final), max_utterance_segment (segment final). On a withheld final: low_confidence_transcript (empty text; see Withheld final above). Treat unknown values as “withheld”.
string | null
Only with diarize: true — dominant speaker label for the final (also speaker_label, speakers).

language_changed — After config message changes language

Other config changes are acknowledged with their own *_changed frame (turn_detector_changed, utterance_end_changed, interim_frequency_changed, audio_enhancement_changed, audio_format_changed, keywords_changed, context_changed, …).

no_speech — The turn had no words

  • terminal: true — the turn is over with no words. Clear any interim text; if you collected segment finals for this turn, dispatch them now. Not billed.
  • terminal: false — an interim check found no words; the turn is still open. Ignore it (or clear the interim display). There is no text field.

turn_complete — End-of-thought confirmed (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 check judged it unfinished.

interim_delta — Delta for an interim (only with interim_delta_streaming: true)

operation: "append" → add text to the line. operation: "replace" → the guess changed; replace the line with full_text.

transcription_tokens — Word tokens (only with token_streaming: true)

Word tokens with stable ids for UIs that animate words. transcription stays the source of truth. event is append for interims, finalize for the final.

warning — Non-fatal notice

Sent once when binary audio arrives without an encoding/sample_rate declared in start (it’s then treated as 8 kHz μ-law).

session_stopped — After stop is processed

Every committed final (is_final: true, non-empty text) is charged by its duration. Because a turn’s several finals cover non-overlapping audio, billing each one sums to the real spoken time with no double-charge. Withheld/empty finals (low_confidence_transcript) and no_speech cost nothing. client_estimated_seconds is a diagnostic estimate of what the client sent; never show it as a billed figure.

error — Rejected message or failed final

Two kinds, told apart by terminal:
  • Non-terminal (no terminal field) — the server rejected a client message (invalid start/config, unknown type, audio-format mismatch). The turn is unaffected; fix the client and resend.
  • Terminal (terminal: true) — a turn’s recognition failed (failure_type such as transcription_timeout, transcription_busy, backend_unavailable, language_identification_uncertain). 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.

test_response — Reply to test ping

Complete Example

    Audio Requirements

    Supported Languages

    45 languages — 12 Indian (as, bn, gu, hi, kn, ml, mr, ne, or, pa, ta, te) plus 33 others including English, Spanish, French, German, Arabic, Mandarin (zh), Japanese (ja), Korean (ko), Turkish (tr), Vietnamese (vi), and Hebrew (he). Fetch the authoritative catalog (with each language’s quality_tier) from GET /v1/languages (or GET /stt/languages via the proxy). Send a small explicit languages list — each extra language is another chance of a wrong pick. 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, …) are rejected — pass ar for best-effort MSA. Common languages:

    Pricing

    • Rate: $0.00000833 per second
    • Minimum: $0.01 per session
    • Billing: Per second of audio processed