Idempotency
Every mutating relay call requires an idempotency key. Here is exactly how reuse behaves.
The rule
Send an Idempotency-Key header on every POST and every WebSocket upgrade:
Idempotency-Key: 5f3a1c9e-7b42-4b8a-9d55-0c6a1f2e8d31The key is opaque to Speko — any non-blank string up to 256 bytes. A UUID per logical request is the right default. Requests without a key are rejected with 400 invalid_request.
Browsers cannot set custom headers on WebSocket upgrades, so the streaming endpoints cannot be called directly from browser JavaScript. Terminate relay WebSockets from your backend.
Content hashing
Idempotency compares your key and a SHA-256 hash of the request content (sha256: + 64 lowercase hex):
- JSON bodies — the raw body bytes exactly as sent.
- Multipart bodies (STT transcriptions) — the decoded payload bytes of the
requestpart, then theaudiopart, concatenated in that order. Part headers and boundary strings are excluded, so honest retries with a new boundary still match. - WebSocket sessions — the exact bytes of the
session.configuretext frame. Frames after configure are outside the hash.
Reuse semantics
| You resend | While | Result |
|---|---|---|
| same key, same content | the original is still being admitted | 409 request_in_progress — retryable; back off briefly and retry |
| same key, same content | the original was already dispatched | 409 request_already_started, carrying the original request_id in the error envelope. The relay is stateless per request and cannot replay the original output. |
| same key, different content | any time | 409 idempotency_conflict — you reused a key for a different request; fix the key |
{
"error": {
"code": "request_already_started",
"message": "a request with this idempotency key has already started",
"retryable": false,
"request_id": "rreq_the_original_request"
}
}Practical guidance
- Generate a fresh key per logical operation and reuse it on retries of that operation — that is what makes network-level retries safe.
- Never reuse a key across different payloads; that is always a
409 idempotency_conflict. - Persist the key alongside your job record so crash-recovery retries stay idempotent.