Speko Docs

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-0c6a1f2e8d31

The 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 request part, then the audio part, 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.configure text frame. Frames after configure are outside the hash.

Reuse semantics

You resendWhileResult
same key, same contentthe original is still being admitted409 request_in_progressretryable; back off briefly and retry
same key, same contentthe original was already dispatched409 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 contentany time409 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.

On this page