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 three payload shapes. All three return the same response schema.

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 lookup (campaign / prerender fixtures)

If you only have the receipt JTI (from X-CloakAPI-Receipt-V2-JTI header or GET /api/v1/receipts), pass it as {"jti": "..."}:

Terminal window
curl -X POST https://api.cloakapi.io/api/v1/receipts/verify \
-H 'Content-Type: application/json' \
-d '{"jti": "01HQABCD1234EFGH56789012"}'

The server resolves the envelope from the receipt chain and returns the same verification response.

Response

{
"valid": true,
"reason": null,
"checks": [
{"label": "Schema valid (OpenReceipt)", "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 (ES256 / ecdsa-p256-sha256)", "status": "ok", "detail": "8.1 ms"},
{"label": "Chain link present", "status": "ok", "detail": "seq 47"}
]
}

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 one of the three recognised shapes, 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

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 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 "signature")
ok = ecdsa_verify(key, sha256(canon), receipt.signature)

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.

Published reference implementations (Rust — FIPS-compliant via aws-lc-rs — plus TypeScript and Python) are at signedreceipts.org/source.