Streaming synthesis
GET /v1/tts/stream — incremental text in, audio out, over WebSocket.
Connecting
Open a WebSocket to wss://relay.speko.dev/v1/tts/stream with Authorization and Idempotency-Key headers on the upgrade, then send session.configure as the first frame (within 10 seconds):
{
"type": "session.configure",
"routing": { "mode": "auto", "objective": "latency" },
"audio": { "encoding": "pcm_s16le", "sample_rate_hz": 24000, "channels": 1 }
}Optionally include "voice" to pin a provider voice ID. As with one-shot synthesis, streaming TTS is currently English-only — see the note on the speech endpoint.
Client frames
| Frame | Meaning |
|---|---|
session.configure | First frame; fixes routing, audio format, and the idempotency hash |
input.append | {"type":"input.append","text":"..."} — add text to synthesize (text non-empty) |
input.commit | Flush: synthesize everything appended so far |
input.cancel | Stop synthesizing the current utterance |
session.close | End the session |
Binary frames from the client are rejected — TTS input is text.
Server frames
Audio arrives as binary frames between utterance.started and utterance.done markers:
| Frame | Meaning |
|---|---|
session.ready | Always first: {"type":"session.ready","request_id":"...","route":{...}} |
utterance.started | {"type":"utterance.started","sequence":1} — sequence is the 1-based utterance index |
| (binary frames) | Audio for the current utterance, in the configured format |
utterance.done | {"type":"utterance.done","sequence":1} |
usage.updated | Running character usage |
session.closed | Clean end, with final usage |
error | Terminal failure — standard error envelope |
Exactly one terminal frame (session.closed or error) ends every session.
Metering and budgets
Characters are counted when an input.append is accepted — before synthesis, and regardless of whether you later cancel. input.cancel stops audio, not billing, for text already accepted.
Streaming sessions admit against an initial character budget and extend it transparently as you append more text. If your organization's credit cannot cover an extension, the next append terminates the stream with budget_exhausted; if the session's lease cannot be renewed, it terminates with lease_expired. Both codes appear only on established streams.
Liveness
The relay pings every 20 seconds. Frames are capped at 1 MiB.