Realtime WebSocket
VoiceRun STT Model is available through the same public realtime protocol as every model on VoiceRun STT Router:
wss://api.voicerun.com/v1/stt
Authenticate with a VoiceRun API key, select voicerun-asr-realtime-v1, and stream audio. Provider credentials are neither required nor accepted.
Connect#
import os import websockets ws = await websockets.connect( "wss://api.voicerun.com/v1/stt", additional_headers={ "Authorization": f"Bearer {os.environ['VOICERUN_API_KEY']}" }, )
The server first emits session.created. Configure the model before sending audio:
{ "type": "session.update", "model": "voicerun-asr-realtime-v1", "fallback_models": ["nova-3", "gpt-4o-transcribe"], "turn_detection": "server", "context": "Dairy Queen; Blizzard; drive-through", "language": "en", "allowed_languages": "en,es", "input_audio_format": "pcm16", "sample_rate": 16000 }
The Router replies with session.updated. It may then emit a warning listing settings that the selected model cannot honor; warnings do not close the session.
Model-specific configuration#
VoiceRun STT Model supports these live controls:
| Field | Description |
|---|---|
context or prompt | Vocabulary and conversational context applied to recognition |
language | Language code, auto, or an empty string to return to auto-detection |
allowed_languages | Comma-separated language allow-list |
eou_threshold | Semantic end-of-turn threshold, as a number or per-language object |
eot_timeout_ms | Hard maximum wait for an end of turn |
vad_stop_ms | Silence before semantic end-of-turn scoring begins |
Send another session.update between turns to update these fields without reconnecting. The public Router keeps the primary model and fallback_models fixed after audio begins.
{ "type": "session.update", "context": "offered times: 6:30 PM, 6:45 PM, 7:00 PM", "language": "en", "eou_threshold": 0.55 }
A successful update produces session.updated. If the active model cannot update settings on a live connection, the Router emits an unsupported_for_model error.
Stream audio#
Append base64-encoded audio chunks. PCM16 little-endian is the default; μ-law is also accepted.
{"type":"input_audio_buffer.append","audio":"AAAA//8AAAEAAAD/////AAAB..."}
Set sample_rate from 8,000 through 48,000 Hz. Chunks around 20–100 ms are a good fit for realtime calls. The Router converts accepted input to the rate required by the active model.
VoiceRun STT Model uses server-side turn-taking. Do not send client commit messages through the public Router. For a finite recording, append trailing silence so the model can close the final turn.
Receive transcripts and turns#
transcription.delta contains the current full hypothesis. Replace the previous hypothesis instead of concatenating it:
{"type":"transcription.delta","text":"I'd like a table","language":"en"}
turn.ended is the completed turn:
{ "type": "turn.ended", "text": "I'd like a table for four.", "language": "en", "reason": "provider" }
See the shared message reference for warnings, errors, fallback, and the complete public event vocabulary.
Automatic fallback#
Configure fallback_models when the session starts. If the active model has a terminal provider failure, the Router can replay up to eight seconds of buffered audio into the next model without replacing the client WebSocket.
{ "type": "session.model_changed", "from_model": "voicerun-asr-realtime-v1", "to_model": "nova-3", "reason": "provider_unavailable", "replayed_audio_ms": 2400 }
Unsupported VoiceRun-specific settings are ignored by a fallback model that cannot apply them, so failover itself is not rejected. See Automatic model fallback.
Complete Python example#
import asyncio import base64 import json import os import websockets URL = "wss://api.voicerun.com/v1/stt" async def transcribe(path: str): async with websockets.connect( URL, additional_headers={ "Authorization": f"Bearer {os.environ['VOICERUN_API_KEY']}" }, ) as ws: created = json.loads(await ws.recv()) assert created["type"] == "session.created" await ws.send(json.dumps({ "type": "session.update", "model": "voicerun-asr-realtime-v1", "fallback_models": ["nova-3"], "turn_detection": "server", "context": "reservation, booking, party size", "language": "en", "input_audio_format": "pcm16", "sample_rate": 16000, })) async def receive(): async for raw in ws: event = json.loads(raw) if event["type"] == "transcription.delta": print("Partial:", event["text"], end="\r") elif event["type"] == "turn.ended": print("Final: ", event["text"]) elif event["type"] == "session.model_changed": print("Fallback:", event["to_model"]) elif event["type"] == "error": raise RuntimeError( f'{event["code"]}: {event["message"]}' ) receiver = asyncio.create_task(receive()) with open(path, "rb") as audio: while chunk := audio.read(3200): await ws.send(json.dumps({ "type": "input_audio_buffer.append", "audio": base64.b64encode(chunk).decode(), })) await asyncio.sleep(0.1) await ws.send(json.dumps({ "type": "input_audio_buffer.append", "audio": base64.b64encode(bytes(32000)).decode(), })) await asyncio.sleep(2) await ws.send(json.dumps({"type": "session.close"})) await receiver asyncio.run(transcribe("audio.pcm"))
The input file in this example is headerless PCM16 little-endian, mono, at 16 kHz.
