Speech to text
Streaming transcription
GET /v1/stt/stream — live transcription over WebSocket.
Connecting
Open a WebSocket to wss://relay.speko.dev/v1/stt/stream with both required headers on the upgrade:
Authorization: Bearer sk_speko_...
Idempotency-Key: <unique key>Browsers cannot set these headers on a WebSocket upgrade — connect from your backend.
Session lifecycle
The first frame you send must be session.configure, within 10 seconds of connecting. Its exact bytes are the idempotency content hash for the session.
{
"type": "session.configure",
"routing": { "mode": "auto", "objective": "latency" },
"audio": { "encoding": "pcm_s16le", "sample_rate_hz": 16000, "channels": 1 },
"language": "en"
}audio.encodingstringrequiredpcm_s16le or opus.
audio.sample_rate_hznumberrequired8000–192000.
audio.channelsnumberrequired1–8.
Then:
- Send audio as binary frames in the configured format.
- Send
{"type":"input.commit"}to signal end of input and ask for final results. - Send
{"type":"session.close"}to end the session.
Server frames
The first server frame is always session.ready:
{ "type": "session.ready", "request_id": "rreq_...", "route": { "provider": "cartesia", "model": "ink-2", "region": "eu-west-1", "attempt_id": "ratt_..." } }| Frame | Meaning |
|---|---|
session.ready | Session admitted and routed; always first |
transcript.delta | Interim hypothesis: {"type":"transcript.delta","text":"..."} (text is always non-empty) |
transcript.final | Finalized text: {"type":"transcript.final","text":"...","segments":[...]} |
usage.updated | Running usage snapshot |
session.closed | Clean end: {"type":"session.closed","usage":{...}} |
error | Terminal failure — the standard error envelope |
Exactly one terminal frame ends every session: session.closed or error. A socket that drops without either was truncated — treat results as incomplete.
Liveness and limits
- The relay pings every 20 seconds; frames are capped at 1 MiB.
- Streaming sessions meter audio duration against a lease that the relay renews internally. If the session's budget or lease cannot be extended, the stream terminates with
budget_exhaustedorlease_expired— these two codes only ever appear on established streams, never as HTTP statuses.