Speko Docs
STT

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/stream

Session 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

FieldTypeDefaultMeaning
type"config"Required. Any other value is rejected.
languageBCP 47 tagkey's policyPicks the board that ranks providers.
interim_resultsbooleanfalseEmit partials before the speaker stops.
stt_options.languageBCP 47 tagOverrides 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.

On this page