Streaming
One WebSocket. Send audio as it arrives, get partial and final transcripts back.
Open a socket, send a config frame, then push audio. Transcripts come back as you speak. For a finished file, use batch — it is one HTTP call.
wss://api.speko.ai/v1/transcribe/streamSession shape, the 5-second config window, ephemeral browser tokens and failover behaviour are identical on both realtime routes — see the realtime contract. Everything below is STT-specific.
Connect
import json
import os
import websockets
async with websockets.connect(
"wss://api.speko.ai/v1/transcribe/stream",
additional_headers={"Authorization": f"Bearer {os.environ['SPEKO_API_KEY']}"},
subprotocols=["speko.realtime.v1"],
) as socket:
await socket.send(json.dumps({"type": "config", "language": "es", "interim_results": True}))
print(await socket.recv()) # {"type":"ready","provider":"Deepgram"}
for chunk in pcm_chunks(): # 16 kHz mono 16-bit little-endian
await socket.send(chunk)
await socket.send(json.dumps({"type": "end"}))
async for message in socket:
event = json.loads(message)
if event["type"] == "transcript" and event["isFinal"]:
print(event["text"])import WebSocket from 'ws';
const socket = new WebSocket('wss://api.speko.ai/v1/transcribe/stream', ['speko.realtime.v1'], {
headers: { Authorization: `Bearer ${process.env.SPEKO_API_KEY}` },
});
socket.on('open', () => {
socket.send(JSON.stringify({ type: 'config', language: 'es', interim_results: true }));
});
socket.on('message', (raw) => {
const event = JSON.parse(raw.toString());
if (event.type === 'ready') startMicrophone((chunk) => socket.send(chunk));
if (event.type === 'transcript' && event.isFinal) console.log(event.text);
});The config frame
| Field | Type | Default | Meaning |
|---|---|---|---|
type | "config" | — | Required. Any other value is rejected. |
language | BCP 47 tag | key's policy | Picks the board that ranks providers. |
interim_results | boolean | false | Emit partials before the speaker stops. |
stt_options.language | BCP 47 tag | — | Overrides language if both are set. |
{ "type": "config", "language": "es", "interim_results": true }Language is resolved narrowest first: stt_options.language, then language, then
X-Speko-Language, then the key's policy. Omit the field and the header or the key decides —
it does not fall back to English.
Audio format
Send 16 kHz mono 16-bit signed little-endian PCM as binary frames. No container and no WAV header — the header would be transcribed as noise at the start of the stream.
Frame size is yours to choose; 20–100 ms per frame is typical for a live agent.
Frames you receive
All are JSON text frames.
{"type": "ready", "provider": "Deepgram"}
{"type": "transcript", "text": "hola, quiero", "isFinal": false, "confidence": 1.0, "words": null}
{"type": "transcript", "text": "Hola, quiero mover mi cita.", "isFinal": true, "confidence": 1.0, "words": null}
{"type": "error", "code": "UPSTREAM", "message": "Streaming STT could not start."}isFinal: false frames are partials — they get replaced, not appended. Render the latest partial and overwrite it when the next one arrives; treat isFinal: true as the committed text for that turn.
confidence is always 1.0 and words is always null today. The vendors
disagree on both — some report per-frame confidence, some only per-word, some
neither — so the router reports a constant rather than a number that means
something different depending on who answered. Do not gate logic on it.
Errors
The three socket codes are the shared ones. Before the upgrade this route answers 503 no_streaming_provider when no streaming-capable STT provider is configured at all.
Which provider answers
The router ranks streaming-capable providers on the live board for your language and dials them in order, advancing on any that fails to connect. The ready frame names the winner.
Your routing applies here as it does on the HTTP routes. X-Speko-Allow, X-Speko-Deny, the objective, the price cap and the key's chain all narrow the set.
A language may also carry an ordered provider preference, tried ahead of the board: Spanish prefers ElevenLabs Scribe, then Soniox. GET /v1/routing/preview?stage=stt&language=<tag> returns the order the socket will use.
Not every provider on the board can stream. A provider qualifies by implementing the streaming contract, not by being listed somewhere, so the set grows without a client change: GET /v1/models plus the ready frame are the current answers.
A pin binds the provider, not the model. The ready frame carries provider and no
model, so pinning one model of a vendor that ships several cannot be confirmed from the
socket. x-route gives provider/model on the HTTP routes.
A pin the transport cannot serve fails the upgrade, not the request: the client sees a
handshake error and close code 1002, with no readable body. Check a pin against
GET /v1/routing/preview first.
Streaming capability is per provider and per language, and the board shows neither. Verify the pin you intend to ship on your own audio. A vendor can accept the socket and then send partials that never finalise, or accept audio and send nothing at all — in both cases without an error frame.
Ending cleanly
Send {"type": "end"} and wait for the last isFinal frame before closing.
Some vendors only flush the tail of the final utterance after they receive their own end-of-stream message, and the router translates end into whichever spelling the serving provider wants. Closing the socket yourself instead of sending end risks losing the last few words — and a truncated transcript reads like the speaker trailed off, so nothing looks broken.