The endpoint
sk-tokay-… key as everything else, either as an Authorization: Bearer … header or — since browsers cannot set headers on a WebSocket — as a ?key= query parameter.
Audio must be PCM16-LE, mono, 16 kHz. Nothing is resampled server-side: guessing the sample rate of a raw byte stream is how audio ends up silently transcribed at the wrong pitch. Resample before you send.
TypeScript
Node 20 has no global
WebSocket — it stabilised in Node 22. On Node 20 either upgrade or pass one in: geko.stt.stream({ webSocket: (await import("ws")).WebSocket }). The SDK itself stays dependency-free.Python
There is no Python SDK for streaming yet; the wire protocol is small enough to use directly withwebsockets:
Partials and finals
Two kinds of result arrive, and the difference matters:
A
final is emitted when the speaker stops — however short the reply was. A one-word “да” finalizes exactly like a full sentence. Committing is also what keeps the cost bounded: once a span is committed it is never re-examined, so a ten-minute call costs no more per minute than a ten-second one.
How quickly they arrive. A new partial is emitted every 0.25s of speech. A final fires once the speaker has been silent for the end-of-utterance threshold, plus roughly 50ms of decode — the decode itself is flat and jitter-free, because every input shape the model can see is pre-warmed when a container boots. Set the threshold yourself (see below) and one value applies at every utterance length: at 200ms a final leaves the server ~250ms after the last word, one-word replies included, and measured end-to-end from Kazakhstan over the public internet it lands ~0.4s after the last word with a worst case near 0.5s. Leave it unset and an accuracy-first adaptive default applies: 350ms, or 700ms for replies under ~2 seconds, a split measured to leave transcription accuracy unchanged. Measured on a warm connection against 16 kHz audio fed at real time; a client that sends larger audio frames cannot receive results faster than it sends, so 250ms frames or smaller are recommended.
Every partial and final also carries audio_to — how far into the audio you have sent the transcript has reached, in seconds. Comparing it with how much you have sent shows whether the transcript is keeping up with the speech, which lets an agent decide when it is safe to act without waiting for the end-of-utterance call at all.
Tuning end of utterance
The threshold is yours to set, per connection — it is the wait between your caller going quiet and their words arriving as afinal, so it sits directly in your response time:
?end_of_utterance_ms=200 on the connection URL, or send {"type":"config","end_of_utterance_ms":200} at any time — the server acknowledges with the value that took effect.
The server clamps to 100–2000ms and echoes the value that took effect in ready, so you never have to assume. Setting it is also a statement about your audio: one threshold then applies at every utterance length. Go low (150–250ms) if your audio is conversational replies and you handle false endpoints downstream — a breath mid-sentence may then split a phrase across two finals, which concatenate to the same transcript but each decode with less context. Go high if you would rather never split a slow, deliberate speaker. If you don’t set it, the adaptive default above applies — tuned for accuracy on long-form speech, at the cost of one-word replies taking ~750ms instead of ~250ms.
We measured the low-threshold trade so you don’t have to guess: on long-form read speech (FLEURS Kazakh), a uniform 200ms threshold costs about +1.9pp WER versus the adaptive default, entirely from mid-sentence splits. On short conversational replies the effect is negligible — a reply that fits between two breaths is a single segment either way. If your callers speak in sentences, not monologues, low thresholds are cheap; if they dictate paragraphs, keep the default.
Turns: when to answer
end_of_turn_ms (query param, config message, or endOfTurnMs in the SDK) is the second
timer, and it answers a different question from the end-of-utterance threshold. A final
means “this transcript is committed” — it fires at a pause, and a pause mid-thought looks
identical to a finished thought at that horizon. end_of_turn means “the human stopped”:
it fires only when the silence has persisted (default 1000ms, clamped 300–5000, 0
disables), measured from the same clock — the end of speech. If the speaker resumes
inside the window, no event fires and the turn keeps accumulating, which is exactly the
false-endpoint insurance an agent otherwise builds client-side. Set it to your
answer-readiness: ~800–1200ms reads natural in conversation.
Context: tell the decoder what this call is about
Pass the entities likely to occur — brands, tariff names, streets — and the decoder favours them when the audio is genuinely ambiguous:DX5 as-is and the decoder is primed for
“дэ икс пять” and “дэ икс бес”. ready (or the config ack) reports context_terms
accepted and names any context_skipped — terms with no expressible spoken form at all.
Biasing is subtractive: a half-matched term earns nothing, so an irrelevant term list
measurably does not bend transcripts (that invariant is part of our release gates).
Numbers as digits
Transcripts are verbatim by default — the model spells numbers out, because that is what was said. If the text feeds an LLM or a CRM, ask for digits:done). Deliberately conservative: a lone small
number word stays a word (“ни один клиент” never becomes “ни 1 клиент”), and the
converter provably inverts the exact normalizers our training references use.
Wire protocol
If you are not using the SDK:1000 and a reason you can act on: stop (you asked), idle (no audio for 30s — reconnect when you have some), or session_limit (25 minutes — reconnect to continue). A close without one of these reasons is the network, not us.
Cold starts
The service scales to zero, so the first connection after an idle period waits while a container comes up. That boot is a GPU memory snapshot restore — the model is already loaded inside the checkpoint — so it takes ~15 seconds, and the container re-warms its decode paths before accepting the socket: the first request on a fresh container behaves exactly like the thousandth. Connections after that open in well under a second, and the first partial arrives about 1s in — a little behind the steady-state 0.5s cadence, because the opening moments of a stream rarely contain enough speech to decode into words yet. Three consequences worth designing for:- Set a generous handshake timeout. Many WebSocket clients default to 10s, which is not enough to survive a cold start. The SDK defaults to 60s and exposes
openTimeout. - Don’t park an idle “spare” socket to dodge the cold start. It doesn’t work — a socket with no audio is closed after 30s — and it holds GPU capacity that other calls could be using. Connect when the call starts; a warm handshake is sub-second.
- For a demo or a call window, ask us to pin a warm container. It removes the cold start entirely. Get in touch.
Pricing
**6 credits per audio-second — 0.36/hour for file transcription. Billed on audio accepted, once, when the socket closes — including if your client disconnects without sendingstop.
Limits
- One socket is one call. A session may run up to 25 minutes, then closes cleanly with reason
session_limit— reconnect to continue. - A socket with no audio for 30s is closed with reason
idle. Silence during a call is fine — keep sending it as frames; the timeout is for sockets nobody is feeding. - Audio not committed before you disconnect is still billed — the server accepted it.
- Transcripts are lowercase and unpunctuated, as with file transcription: the model’s output vocabulary contains neither. See accuracy.