Realtime STT
Streaming guide βLow-latency streaming transcription over a single WebSocket connection: connect, send a start frame, stream raw audio, send an end frame to flush, and receive result messages (plus lifecycle events) as each utterance is transcribed. After the stream ends, retrieve the stored session result with the REST endpoint documented below.
WebSocket connection
| Field | Value |
|---|---|
| Endpoint | wss://api.denthub.ai/stt/realtime/stream |
| Query | ?key=<key> |
| Audio | PCM16LE Β· 16000 Hz Β· mono Β· binary frames |
| Max conns/pod | 200 (HTTP 503 when exceeded) |
startβ first frame (text)
| Field | Type | Required | Value |
|---|---|---|---|
| type | string | yes | "start" |
| vad | object | no | segmentation tuning β see below |
sample_rate/codec/channels is unnecessary (accepted only if they match what the server expects, kept around for legacy clients). lang/backend are also fixed server-side (English STT, densper3) β any client-sent values are ignored. num_speakers/resume_* are reserved fields: accepted, but currently a no-op.vad options
| Field | Default | Range | Meaning |
|---|---|---|---|
| min_silence_ms | 640 | 100β3000 | silence that ends an utterance (lower = fast cuts, higher = merge sentences) |
| speech_pad_ms | 200 | 0β1000 | padding kept around each clip (protects onset/offset phonemes) |
| threshold | 0.6 | 0.05β0.95 | speech-start sensitivity (lower = more sensitive, higher = conservative) |
| min_speech_ms | 192 | 32β2000 | shorter blips are treated as noise |
threshold must be β₯ 0.4, and 2 Γ speech_pad_ms must be β€ min_silence_ms. vad is only valid in the first start frame, before any audio β it cannot change mid-stream. Invalid values are rejected outright (error frame + connection close): the sent value is always the applied value, there is no silent correction.Playground presets
| Preset | vad |
|---|---|
| Fast cuts | {"min_silence_ms":200,"speech_pad_ms":100,"threshold":0.6,"min_speech_ms":192} |
| Default | {"min_silence_ms":300,"speech_pad_ms":100,"threshold":0.6,"min_speech_ms":192} |
vad snapshot. Its Default preset is an editable starting point and does not replace the server defaults listed above.{ "type": "start" }{
"type": "start",
"vad": { "min_silence_ms": 400, "speech_pad_ms": 150 }
}audioβ binary frames
Send raw PCM16LE bytes as binary WebSocket frames β no WAV header, no base64, no JSON wrapping. Any frame size works; send continuously, without gating on silence yourself.
The gateway re-frames your stream internally into 32Β ms VAD windows and detects utterance boundaries server-side, so you never need to run your own voice-activity detection.
endβ graceful close (text)
Send this text frame to flush any pending utterance and close cleanly. After sending it, you may close the WebSocket yourself, or wait for the server to close it once flushing finishes.
{ "type": "end" }resultβ one per utterance
{
"type": "result",
"utt_index": 0,
"session_id": "b1f7β¦",
"abs_start_utc": "2026-07-10T05:12:01.80Z",
"abs_end_utc": "2026-07-10T05:12:05.20Z",
"duration_ms": 3400,
"text": "Patient presents with sensitivity on the upper right quadrant.",
"segments": [
{ "start_ms": 800, "end_ms": 4200,
"text": "Patient presents with sensitivityβ¦", "language": "en" }
],
"words": [ { "word": "Patient", "start_ms": 800, "end_ms": 1180 }, β¦ ],
"status": "ok",
"forced_cut": false,
"continues": false
}| Field | Type | Description |
|---|---|---|
| utt_index | int | 0-based, monotonic per connection |
| session_id | uuid | Connection session ID |
| abs_start_utc / abs_end_utc | string (ISO 8601) | Absolute utterance start/end (UTC) |
| duration_ms | int | Utterance duration (sample-derived) |
| text | string | Full transcription text |
| segments[] | array | start_ms / end_ms / text / language, recording-relative |
| words[] | array | word / start_ms / end_ms, recording-relative |
| status | string | "ok" Β· "empty" Β· "failed" |
| forced_cut | bool | true if the utterance was force-split at max length |
| continues | bool | true if the next utt_index continues this utterance |
abs_*_utc, and each start_ms/end_ms inside segments[] and words[] β is recording-relative, derived from a sample-count clock, not message-arrival time. Use these to line results up against the audio you sent, even under network jitter.β session Β· warning Β· error
{"type":"session","event":"started","session_id":"β¦"} // started Β· ended
{"type":"warning","code":"SLOW_CONSUMER"} // backpressure
{"type":"utterance_dropped","utt_index":8,"code":"STT_OVERLOAD"}
{"type":"error","utt_index":9,"code":"STT_TIMEOUT","message":"β¦","retryable":true}- session β connection lifecycle:
startedandended. - warning β backpressure signal (
SLOW_CONSUMER): you're sending audio faster than the gateway can process; slow down or check your network. - utterance_dropped β the STT backend was overloaded and this utterance was skipped; the gap in
utt_indextells you which one. - error β a per-utterance failure; check
retryableto decide whether to wait or move on.
Error codes
The error event carries a code and a retryable flag. Non-retryable errors close the connection; retryable ones are per-utterance.
| Code | Meaning | Retryable |
|---|---|---|
| INVALID_FRAME | A start value broke a rule β a vad range/cross-rule violation, or an audio-format field that doesn't match the fixed contract | No β fix and reconnect |
| STT_TIMEOUT | The STT backend timed out on this utterance | Yes β wait; the backend retries |
| STT_BACKEND_5XX | The STT backend returned a 5xx for this utterance | Yes |
| STT_PARSE_ERROR | The backend response could not be parsed | No β log and continue |
| INTERNAL | Unexpected server-side error | No β log and reconnect |
warning events are non-fatal backpressure signals β the socket stays open:
| Code | Meaning |
|---|---|
| SLOW_CONSUMER | You're sending audio faster than the gateway can process β slow down or check your network |
| RING_BUFFER_NEAR_FULL | The ~3 s audio buffer is nearly full and audio may start dropping β slow down |
start β it sends an error frame and closes; what you send is what applies.Close codes
| Code | Meaning |
|---|---|
| 1000 | Normal close, after you sent end |
| 1001 | Going away β server is draining; reconnect to a new session |
| 1011 | Internal error β log it and reconnect |
?key= causes the WebSocket handshake itself to be rejected with HTTP 401 β the socket never opens. This is distinct from the 1000 / 1001 / 1011 close codes, which apply only to an already-open connection.Retrieve session result
After the WebSocket stream ends, fetch the stored session record β this is how you retrieve the finalized recording and the billed usage once streaming is done. The response shape is below.
api.denthub.ai/stt/realtime/{sessionId}Auth: this REST call uses the X-License-Key header (unlike the WS handshake, which uses the ?key= query param β browsers can't set WS headers). Returns 404 if the session does not exist.
Path parameters
| Field | Type | Notes |
|---|---|---|
| sessionId | string (uuid) | required |
curl https://api.denthub.ai/stt/realtime/<sessionId> \
-H "X-License-Key: <license-key>"{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"quota": 42.5,
"unit": "seconds",
"recordingUrl": "recordings/2026/07/13/<session>.wav",
"segments": [
{ "startMs": 0, "endMs": 3200, "result": {}, "isFinal": true }
]
}Response fields
| Field | Type | Notes |
|---|---|---|
| sessionId | string | the realtime session id |
| status | enum | PENDING Β· PROCESSING Β· COMPLETED Β· FAILED Β· ERROR Β· PARTIAL |
| quota | number | billable usage (max segment end time) |
| unit | string | quota unit (e.g. seconds) |
| recordingUrl | string | object-storage key of the finalized recording |
| segments[] | object[] | stored segments β { startMs, endMs, result, isFinal } |
result message: segments here use camelCase fields (startMs/endMs) as integer milliseconds, not the WS message's snake_case start_ms/end_ms.