Skip to content

Verifying a receipt

In the browser

The fastest way is the public verifier at app.cloakapi.io/receipt-verifier. Paste a receipt JSON, press Verify. No login required. The page calls the public POST /api/v1/receipts/verify endpoint and renders a per-step breakdown.

From the CLI

POST /api/v1/receipts/verify accepts an envelope in two payload shapes (A and B), which return the same response schema. Shape C explains what happens with a JTI.

Pass the receipt envelope as the root JSON object:

Terminal window
curl -X POST https://api.cloakapi.io/api/v1/receipts/verify \
-H 'Content-Type: application/json' \
-d @receipt.json

Shape B — wrapped in receipt key

Terminal window
curl -X POST https://api.cloakapi.io/api/v1/receipts/verify \
-H 'Content-Type: application/json' \
-d "{\"receipt\": $(cat receipt.json)}"

Shape C — JTI only

If you only have the receipt JTI (from the X-CloakAPI-Receipt-V2-JTI header), the verify endpoint will not resolve it for you: {"jti": "..."} answers 401 auth_required, with or without a cloak_ API key. GET /api/v1/receipts/<jti> accepts only a signed-in portal session, not an API key. Keep the full X-CloakAPI-Receipt-V2 header value from the call and use shape A or B.

Response

{
"valid": true,
"reason": null,
"checks": [
{"label": "Schema valid (OpenReceipt v1)", "status": "ok", "detail": "0.4 ms"},
{"label": "Algorithm allow-listed", "status": "ok", "detail": "ES256"},
{"label": "Freshness (iat / nbf / exp)", "status": "ok", "detail": "iat=1748000000 within window"},
{"label": "Signature (ecdsa-p256-sha256)", "status": "ok", "detail": "8.1 ms"},
{"label": "Chain linkage (seq / prev_hash invariant)", "status": "ok", "detail": "seq 47 links prev_hash 2b9ff4ec3c87…"}
]
}

200 means valid; 422 with valid: false and a per-step checks array means at least one check failed — the checks array tells you which step and why.

If the payload is not a recognised shape, the API returns 400 with {"error": {"code": "invalid_payload", "message": "..."}}.

Decoding the X-CloakAPI-Receipt-V2 header

The X-CloakAPI-Receipt-V2 response header is a base64-encoded JSON blob. Decode it to get the envelope before posting to verify:

Terminal window
HEADER_VALUE="<paste base64 from response header here>"
echo "$HEADER_VALUE" | base64 -d > receipt.json
curl -X POST https://api.cloakapi.io/api/v1/receipts/verify \
-H 'Content-Type: application/json' \
-d @receipt.json

Reproducing hash_upstream_request

hash_upstream_request (a top-level field of the gateway’s v2 envelope) is the SHA-256 (lowercase hex) of the RFC 8785 JCS canonical form of the JSON request body the gateway relays to the provider. For the relay endpoints (/v1/messages, /v1/chat/completions, /v1/embeddings) that body is your request body, unchanged: string values are not trimmed and empty strings are not converted to null. To check it, canonicalise the JSON you sent with JCS and hash the result:

import canonicalize from 'canonicalize';
import { createHash } from 'node:crypto';
const h = createHash('sha256').update(canonicalize(JSON.parse(sentBody))).digest('hex');
// h === receipt.hash_upstream_request (receipt = the decoded v2 envelope)

It is a hash of the canonical JSON, not of the raw bytes, so key order and whitespace between tokens do not matter. Whitespace inside string values does.

In your own code

A receipt is just a signed JSON object. Any standard ECDSA-P-256-SHA-256 verifier works — the gateway is just a convenience wrapper.

Pseudocode (gateway-signed v1 or v2 envelope):

jwks = HTTP GET https://api.cloakapi.io/api/.well-known/cloakapi-receipt-pubkeys.jwks
key = jwks.keys.find(k => k.kid == receipt.kid)
canon = json_canonicalize(receipt without "sig" and "signature")
ok = ecdsa_verify(key, sha256(canon), base64url_decode(receipt.sig)) # raw r||s

Client-signed v3 envelopes are self-contained: the attester public key is embedded in claims["v3-attestation"].attester_public_key, so verification works fully offline with no JWKS fetch — canonicalise the envelope minus sig, SHA-256, verify against the embedded key. See the v3 specification §8.2.

A live in-browser verifier is published at signedreceipts.org/verifier. Reference implementations (Rust — FIPS path via aws-lc-rs — plus TypeScript and a verifier CLI) are described at signedreceipts.org/source; their source distribution is being prepared and is not yet published. A Python reference library is planned.