Skip to content

OpenReceipt protocol

The OpenReceipt protocol is an open spec — read the canonical version at signedreceipts.org/spec. This page summarises how CloakAPI implements it.

Envelopes in production

CloakAPI issues three envelope kinds today, depending on the way you use:

  • v3 (client-signed) — on the client-side ways (browser chat, desktop app, browser extension), the device that performed the tokenisation signs an OpenReceipt v3 envelope (v: "v3", alg: "ecdsa-p256-sha256") before egress, carrying the v3-attestation claim set (what egressed, which detector categories fired, engine hashes, trust tier). When the request goes through the gateway, the gateway verifies the client signature and adds an additive countersignature witnessing provider, model, and upstream byte hashes. See the v3 specification for the normative format.
  • v2 (gateway-signed) — on the bare-SDK API way (api.cloakapi.io), the gateway seals each completed response with the v2 flat envelope described below and returns it in the X-CloakAPI-Receipt-v2 header (or as the receipt-v2 event at the end of a stream — a stream cut off before it completes has no trailer). Authenticated calls that fail with a 4xx/5xx get a separate signed failed-call receipt; a request with no valid API key gets none.
  • v1 (gateway-signed, stored) — the gateway also writes a v1 envelope to your receipt chain for every call. This is the receipt GET /api/v1/receipts and the receipt verifier’s JTI lookup return. Its shape is described in v1 envelope shape below.

v2 envelope shape

The v2 envelope is a flat JSON object signed with ES256 (ECDSA P-256 over SHA-256), canonicalised per RFC 8785 (JCS), and chain-linked per tenant.

{
"v": 2,
"alg": "ES256",
"kid": "gw-prod-2026-q2",
"iss": "https://api.cloakapi.io",
"jti": "1de5882f66e6e376c7b26ce2c34bafc7",
"iat": 1759420800,
"timestamp": "2026-04-18T12:00:00.000Z",
"request_id": "req_01HWRK9GTBF3MXQJ4VZPND",
"provider": "openai",
"model": "gpt-4o",
"input_tokens": 184,
"output_tokens": 312,
"hash_upstream_request": "b3a…",
"hash_upstream_response": "c91…",
"chain": {
"chain_id": "receipt-v2-org-42",
"seq": 47,
"prev_hash": "sha256:a3f8c2…"
},
"signature": "BASE64URL_RAW_R_S_ECDSA_SIGNATURE",
"sig": "BASE64URL_RAW_R_S_ECDSA_SIGNATURE"
}
FieldMeaning
vProtocol version. Integer 2 for the gateway-signed flat envelope (client-signed receipts use string "v3").
algSigning algorithm. "ES256" (ECDSA P-256 over SHA-256).
kidKey id — look up the matching key in the public JWKS to get the verifying key.
issIssuer URL (https://api.cloakapi.io).
jtiUnique receipt id — 32 lowercase hex characters (128 random bits). The transparency feed surfaces the same value as receipt_id.
iatUnix-seconds timestamp the gateway sealed the response.
timestampRFC 3339 timestamp with millisecond precision, always UTC (Z).
provider / modelUpstream provider slug and model the call was routed to.
routing_decisionPresent when the router ran: from, to (provider slugs), chosen_model, classifier, savings_pct, strategy. There is no provider_id field.
input_tokens / output_tokensToken counts billed for the call.
hash_upstream_request / hash_upstream_responseSHA-256 (bare lowercase hex), never the bytes themselves. hash_upstream_request covers the JCS-canonical (RFC 8785) form of the JSON body relayed to the provider, which is the client’s body unchanged; see Verify → Reproducing hash_upstream_request.
chain.chain_idPer-tenant chain identifier (receipt-v2-org-<org_id>).
chain.seqMonotonically increasing per-tenant sequence number.
chain.prev_hashHash of the previous receipt’s canonical bytes (same chain). Lets auditors detect missing or out-of-order receipts.
signature / sigBoth carry the same value: URL-safe base64 (unpadded) raw r‖s (64-byte) ECDSA signature over the JCS-canonical bytes of the envelope with both signature and sig removed. sig is the OpenReceipt name the SDKs read.

v1 envelope shape

The stored receipt nests the call data under claims. Abridged from a live receipt:

{
"v": "v1",
"alg": "ecdsa-p256-sha256",
"kid": "gw-prod-2026-q2",
"iss": "https://api.cloakapi.io",
"sub": "api_key:1055",
"jti": "1de5882f66e6e376c7b26ce2c34bafc7",
"iat": 1789978391,
"chain": { "chain_id": "tenant-51", "seq": 1078, "prev_hash": "7a08…" },
"claims": {
"capability": "chat",
"provider": "anthropic",
"model": "claude-haiku-4-5-20251001",
"tokens_in": 287,
"tokens_out": 42,
"hash_upstream_request": "7ee1…",
"hash_upstream_response": "3dc2…",
"routing_decision": {
"from": "anthropic", "to": "anthropic",
"chosen_model": "claude-haiku-4-5-20251001",
"strategy": "client-side", "classifier": "client-side-arbitrage"
},
"tokenisation_mode": "client_pretokenised",
"engine_version": "1.6.0"
},
"sig": "BASE64URL_RAW_R_S_ECDSA_SIGNATURE"
}
  • The v1 envelope is signed by the gateway (the kid resolves in the same JWKS as v2), not by your device — it has no trust tier.
  • Token counts are claims.tokens_in / claims.tokens_out; hashes are bare hex without a sha256: prefix; chain.chain_id is tenant-<account id>.
  • Signature: sig over the JCS-canonical bytes of the envelope with sig removed, verified with the JWKS key matching kid.

Where to find the public key

  • JWKS: https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkeys.jwks
  • PEM: https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkey.pem

Keys rotate quarterly. Old keys remain in the JWKS so historic receipts keep verifying indefinitely.