Skip to content

Verify a receipt

Completed non-streaming model calls may include an X-CloakAPI-Receipt-V2 response header when a receipt is produced — a base64-encoded (standard alphabet, padded) JSON OpenReceipt envelope. You get it on calls that reach the gateway with the client proof headers expected for pre-tokenised relay (SDKs, the local proxy, or the contract in Chat completions). The local proxy 0.3.0 does not pass this header through to your client.

Verify online (no account)

Pass the header value verbatim:

Terminal window
# $RECEIPT = the entire X-CloakAPI-Receipt-V2 header value
curl -X POST https://api.cloakapi.io/api/v1/receipts/verify \
-H 'Content-Type: application/json' \
-d "{\"receipt\": \"$RECEIPT\"}"

The gateway accepts three payload shapes:

  1. Header value (base64 string) — {"receipt": "<header>"} (recommended; least typing)
  2. Decoded envelope object — {"receipt": {"v":2,"alg":"ES256",...}}
  3. JTI lookup — {"jti": "<jti>"} answers 401 auth_required, with or without a cloak_ key; GET /api/v1/receipts/<jti> accepts only a signed-in portal session

Or paste the envelope into the browser-based verifier at app.cloakapi.io/receipt-verifier.

A valid: true response means:

  • the schema is intact,
  • the signature was made by a key listed in the public JWKS (https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkeys.jwks),
  • the chain prev_hash links into the previous receipt for the same tenant.

Offline verification (no gateway round-trip)

The envelope is independently verifiable. Decode the base64 header, canonicalise the JSON (sort keys, no whitespace), drop the sig and signature fields (both carry the same signature), SHA-256 it, then ECDSA-P256 verify with the public key whose kid matches envelope.kid in the JWKS. Read the envelope from the x-cloakapi-receipt-v2 response header of the call that produced it.

A reference Python verifier in 30 lines (uses cryptography):

import base64, json, hashlib, urllib.request
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
from cryptography.hazmat.primitives import hashes
def b64u(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
env = json.loads(b64u(receipt_header_value))
jwks = json.loads(urllib.request.urlopen(
"https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkeys.jwks").read())
key = next(k for k in jwks["keys"] if k["kid"] == env["kid"])
env.pop("signature", None) # the envelope carries the signature twice
sig = b64u(env.pop("sig")) # drop both before canonicalising
canonical = json.dumps(env, sort_keys=True, separators=(",", ":")).encode()
pub = ec.EllipticCurvePublicNumbers(
int.from_bytes(b64u(key["x"]), "big"),
int.from_bytes(b64u(key["y"]), "big"),
ec.SECP256R1(),
).public_key()
pub.verify(
encode_dss_signature(int.from_bytes(sig[:32], "big"), int.from_bytes(sig[32:], "big")),
canonical,
ec.ECDSA(hashes.SHA256()),
)
print("PASS")

That’s it — completed replies can carry a signed receipt when one is produced. A receipt proves the record is intact and was signed by us; it does not by itself prove what was or was not detected in your text.