Chat completions
POST /api/v1/chat/completions accepts OpenAI-compatible chat-completions payloads
and supports streaming (Server-Sent Events) and Idempotency-Key semantics that
stop a completed call from running or billing twice. A stock OpenAI SDK pointed at
https://api.cloakapi.io/api/v1 is not sufficient by itself: the gateway also
requires client-side tokenisation and proof headers. Use a CloakAPI SDK or the
local proxy, or implement the proof flow shown below in
your own client.
X-Cloak-Pretokenised
Setting this header is an assertion you make about your own request body. The gateway verifies the tenant-bound MAC over the bytes it receives; it does not verify that a tokeniser ran, that every identifier was detected, or that the body contains no raw PII.
The gateway requires the proof headers below. A request without them is refused and nothing
is sent upstream: no engine identity → 428 engine_identity_required or
403 engine_untrusted; no pre-tokenised proof → 428 pretokenised_required; a MAC that does not
match the bytes → 403 pretokenised_mac_invalid. The local proxy and the
SDKs send all of this for you. A successful check authenticates the submitted bytes and
declared identity; it does not inspect message content or establish that all PII was replaced.
Calling the endpoint by hand looks like this:
# $BODY must already be tokenised on your device, by an official CloakAPI engine.BODY='{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"Hello"}]}'
# 1. MAC over the exact bytes you will send (tenant-bound, from your key)MAC=$(curl -sS -X POST https://api.cloakapi.io/api/v1/pretokenised/mac \ -H "Authorization: Bearer $CLOAKAPI_API_KEY" \ -H "Content-Type: application/json" \ --data-binary "$BODY" | jq -r .mac)
# 2. Send the SAME bytes with the proof and the engine identitycurl https://api.cloakapi.io/api/v1/chat/completions \ -H "Authorization: Bearer $CLOAKAPI_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Cloak-Pretokenised: 1" \ -H "X-Cloak-Client-MAC: $MAC" \ -H "X-Cloak-Engine-Version: $CLOAK_ENGINE_VERSION" \ -H "X-Cloak-Engine-Hash: $CLOAK_ENGINE_HASH" \ --data-binary "$BODY"$CLOAK_ENGINE_VERSION and $CLOAK_ENGINE_HASH are the version and binary sha256 of the
official CloakAPI engine that tokenised $BODY (minimum version 1.6.0; the accepted hashes are
listed at GET https://api.cloakapi.io/api/v1/engine/hashes). Send the body with
--data-binary, byte for byte: any change after the MAC was computed makes it invalid.
Idempotency-Key
A flaky connection that triggers a client-side retry must not double-bill the user or duplicate
the receipt-chain entry. On this endpoint the gateway does not keep the response body: a retry
with the same key and body is refused with 409, not replayed (see the contract below). CloakAPI implements the IETF-draft Idempotency-Key header
(draft-ietf-httpapi-idempotency-key-header).
Add the header to the pre-tokenised call above ($BODY and $MAC as computed there):
curl https://api.cloakapi.io/api/v1/chat/completions \ -H "Authorization: Bearer $CLOAKAPI_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Cloak-Pretokenised: 1" \ -H "X-Cloak-Client-MAC: $MAC" \ -H "X-Cloak-Engine-Version: $CLOAK_ENGINE_VERSION" \ -H "X-Cloak-Engine-Hash: $CLOAK_ENGINE_HASH" \ -H "Idempotency-Key: $(uuidgen)" \ --data-binary "$BODY"Contract
| Aspect | Behaviour |
|---|---|
| Header name | Idempotency-Key |
| Recommended format | UUID v4 |
| Max length | 128 chars |
| Scope | Per organisation (tenant); also bound to the request body — the same key with a different body returns 409 |
| TTL | 24 hours; an expired key is treated as fresh |
| What is kept | A body-free completion marker and a digest of the request body. The response body (including a streamed transcript) is never retained on chat/completions, messages, embeddings. |
| Same key, same body, after the first call finished | 409 idempotency_response_not_retained. No second provider call, no second debit; the original response is not returned again |
| Same key, different body | 409 idempotency_key_conflict |
| Same key, same body, first call still running | The duplicate waits up to 3 seconds, then gets 409 idempotency_request_in_progress. Retry with the same key later |
| Failed first call | A call that failed before consuming anything leaves the key retryable |
| Response headers | Idempotency-Key-Used, Idempotency-Replayed and X-CloakAPI-Idempotent-Replay are sent as false on the first call. No relay response is ever replayed |
Concurrent retries — if the original call is still in flight when a duplicate arrives,
the duplicate waits up to 3 seconds and then returns 409 idempotency_request_in_progress.
The gateway never starts a second upstream call for the same key.
Receipt / chain semantics — a retry does not run the call again, so the chain sequence does
not advance and the user’s balance is not debited a second time. Because the response is
not kept, the retry does not return the original receipt either: store the
X-CloakAPI-Receipt-V2 header from the first response yourself. To check a receipt you saved:
curl -s -X POST https://api.cloakapi.io/api/v1/receipts/verify \ -H 'Content-Type: application/json' \ -d '{"receipt":"<x-cloakapi-receipt-v2 header from the original response>"}'Streaming (SSE)
Pass "stream": true to receive an OpenAI-style Server-Sent Events stream. The body is still
pre-tokenised and MAC’d exactly as above; the MAC covers the body including "stream": true.
(The local proxy 0.3.0 does not stream: it answers "stream": true with HTTP 501.)
BODY='{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"Count to three"}],"stream":true}'MAC=$(curl -sS -X POST https://api.cloakapi.io/api/v1/pretokenised/mac \ -H "Authorization: Bearer $CLOAKAPI_API_KEY" \ -H "Content-Type: application/json" \ --data-binary "$BODY" | jq -r .mac)
curl -N https://api.cloakapi.io/api/v1/chat/completions \ -H "Authorization: Bearer $CLOAKAPI_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Cloak-Pretokenised: 1" \ -H "X-Cloak-Client-MAC: $MAC" \ -H "X-Cloak-Engine-Version: $CLOAK_ENGINE_VERSION" \ -H "X-Cloak-Engine-Hash: $CLOAK_ENGINE_HASH" \ --data-binary "$BODY"Response is Content-Type: text/event-stream; charset=utf-8 with frames of the shape:
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"1"}}]}
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":" 2"}}]}
data: [DONE]
event: receipt-v2data: {"alg":"ES256","jti":"...","hash_upstream_response":"...","input_tokens":...,"output_tokens":...,...}The terminator is data: [DONE]\n\n. After it, the gateway emits a single
event: receipt-v2 frame carrying the same receipt envelope you would receive in the
X-CloakAPI-Receipt-V2 header for a non-streamed call. Pass the envelope’s data: payload
to /api/v1/receipts/verify to confirm the signature, the upstream-response hash, and the
chain position — the verifier endpoint does not care whether the receipt originated from a
streamed or non-streamed call.
Streaming + Idempotency-Key
Streamed calls honour Idempotency-Key too, with the same contract as the non-streamed path.
The gateway does not record the SSE transcript. The key stays reserved while the stream runs; a
stream that failed before delivering any frame or billable token leaves the key retryable, while a
stream that delivered output (or was billed) is marked done, so a retry cannot be a second free
completion.
KEY=$(uuidgen)curl -N -H "Idempotency-Key: $KEY" ... # first call: real upstream streamcurl -N -H "Idempotency-Key: $KEY" ... # retry after completion: 409 idempotency_response_not_retained, no debitA different request body with the same key returns 409 idempotency_key_conflict, exactly like
the non-streamed path.
Edge buffering
api.cloakapi.io advertises Cache-Control: no-cache, no-store, must-revalidate and
X-Accel-Buffering: no on streamed responses, and the front nginx is configured with
proxy_buffering off for the chat-completions location. This means individual chunks reach
the client as soon as they arrive from the upstream provider — there is no edge-layer buffer
to drain.
If you are running CloakAPI behind your own reverse proxy (Cloudflare, AWS ALB, etc.), make
sure that proxy also passes SSE through unbuffered. For Cloudflare you typically need a Page
Rule with Cache Level: Bypass for /api/v1/chat/completions.
Reference matrix
| Behaviour | Non-stream | Stream |
|---|---|---|
Idempotency-Key honoured | yes | yes |
| Response replayed on retry | no (409 idempotency_response_not_retained) | no (same) |
| Conflict (different body) | 409 idempotency_key_conflict | 409 idempotency_key_conflict |
| Receipt delivery | X-CloakAPI-Receipt-V2 header | event: receipt-v2 SSE frame |
| Verifier endpoint | /api/v1/receipts/verify | /api/v1/receipts/verify |
| Chain seq advances on retry | no | no |
| Balance debited on retry | no | no |
Related
- Receipts protocol — receipt envelope format
- Webhooks — outbound delivery with its own
Idempotency-Keycontract