{"$schema":"https://certifieddata.io/schemas/receipt-verification-spec.v1.json","spec_version":"1.0","title":"CertifiedData Agent Commerce receipt verification","updated":"2026-08-20","human_readable":"https://certifieddata.io/verify/payment-receipt","repository":"https://github.com/certifieddata/verify","license":"MIT","summary":"How to verify a CertifiedData Agent Commerce payment receipt without trusting CertifiedData. Two unauthenticated inputs, RFC 8785 canonicalization, one Ed25519 signature check.","inputs":{"receipt_envelope":{"endpoint":"https://certifieddata.io/api/payments/verify/{receipt_id}","method":"GET","authentication":"none","trust_required":"none — tampering is precisely what the signature detects, so a hostile response cannot produce a false VALID"},"public_key":{"endpoint":"https://certifieddata.io/.well-known/certifieddata-public-key.pem","method":"GET","authentication":"none","format":"PEM, SPKI-encoded Ed25519 public key","trust_required":"that this key belongs to CertifiedData","note":"This is a different trust root from the certificate keys document at /.well-known/signing-keys.json. Receipts and certificates are separate artifacts."}},"not_evidence":{"fields":["valid","hashValid","signatureValid"],"reason":"These are CertifiedData's opinion about CertifiedData's own signature. A verifier MUST compute its own verdict. Use them at most as a cross-check: disagreement means one implementation has a bug."},"canonical_payload":{"value":"the envelope's `receipt` field, exactly as returned","add_nothing":true,"remove_nothing":true,"commonly_mistaken_for_part_of_it":[{"field":"signature","location":"envelope level, a sibling of `receipt`","reason":"it cannot be part of the payload it signs"},{"field":"sha256_hash","location":"appended by POST /v1/transactions/{id}/capture to its inline receipt object","reason":"convenience only, NOT part of the signed payload. Strip it when verifying a capture response; the verify endpoint does not include it."},{"field":"ed25519_sig","location":"appended by POST /v1/transactions/{id}/capture to its inline receipt object","reason":"same as sha256_hash"}],"producer_note":"The platform applies a stripUndefined() pass before canonicalizing. This has no effect on a consumer, because JSON has no undefined — a key is either present or absent. Documented only so the two implementations can be compared line by line."},"canonicalization":{"algorithm":"RFC 8785","name":"JSON Canonicalization Scheme (JCS)","reference":"https://www.rfc-editor.org/rfc/rfc8785","encoding":"UTF-8","not":"json-stable-stringify","why_not":"JCS and json-stable-stringify agree on key ordering for simple documents but differ on string escaping and number formatting. They can therefore produce different bytes for the same document, and different hashes.","rules":{"object_keys":"sorted ascending by UTF-16 code unit sequence","arrays":"order preserved","whitespace":"none, anywhere","string_escapes":"minimal RFC 8259 section 7 escapes (quote, backslash, \\b, \\f, \\n, \\r, \\t) plus \\u00XX for other control characters U+0000 through U+001F; non-ASCII emitted literally, never \\u-escaped","numbers":"ECMAScript Number::toString, which is what JSON.stringify emits for finite numbers; NaN and infinities MUST NOT appear"}},"checks":[{"name":"hash","procedure":"sha256(canonical_bytes), lowercase hex, prefixed 'sha256:'","compare_to":"envelope.storedReceiptHash","must":"be equal"},{"name":"signature","procedure":"base64-decode envelope.signature (exactly 64 bytes) and verify as Ed25519 over canonical_bytes directly","key":"the SPKI public key from the PEM","must":"verify","warning":"Do NOT pre-hash the payload. Ed25519 hashes internally; passing the SHA-256 digest as the message will fail."}],"verdict":{"VALID":"both checks pass","INVALID":"canonical bytes were produced but a check failed","MALFORMED":"the artifact is unusable — e.g. the signature is not 64 bytes","UNKNOWN_KEY":"the signing key could not be resolved or is not trusted","NETWORK":"an input could not be fetched — never degrade this to accepting the server's booleans"},"test_vectors":{"base_url":"https://raw.githubusercontent.com/certifieddata/verify/main/fixtures/","vectors":[{"file":"valid-receipt.json","expect":"VALID","note":"captured from production"},{"file":"tampered-receipt.json","expect":"INVALID","note":"`amount` altered, signature left byte-identical. The decisive vector: an implementation reporting VALID here is not verifying anything."},{"file":"malformed-receipt.json","expect":"MALFORMED","note":"signature is not a 64-byte Ed25519 value"}]},"worked_example":{"receipt_id":"2492a060-8fbc-40ae-beab-7258aefb0608","description":"a $0.99 certificate-linked dataset purchase on the live rail","captured":"2026-08-20","signing_key_id":"ed25519-prod-2025-02","canonicalization":"RFC8785-JCS","stored_receipt_hash":"sha256:2e14cf92c38d5d0cf2b577c4736404fad1c1092c3c4ef87e3b4efeb3923dde22","settlement_state":"succeeded_live","artifact_hash":"sha256:bd48985485c9a3e19838e29795bb89ddedd7f7e5c706b57c190cc6c46119a660","certificate_id":"fb914a90-b1b3-4355-8147-cc0194160e23","chain":"The artifact_hash is the SHA-256 of the delivered ZIP and also the digest inside certificate fb914a90. A buyer can chain downloaded bytes -> hash -> certificate -> receipt with no step requiring CertifiedData's word.","stale_key_id_warning":"Some older documentation shows signingKeyId 'cd_root_2026'. Live receipts are signed with ed25519-prod-2025-02."},"tooling":{"run_without_install":"npx github:certifieddata/verify {receipt_id} --type receipt","run_from_clone":"git clone https://github.com/certifieddata/verify && node verify/dist/cli.js {receipt_id} --type receipt","no_build_required":"dist/ is committed, so a clone runs with no npm install and no toolchain","accepted_inputs":{"bare_uuid":"fetched from the public verify endpoint","local_file_path":"supported","stdin":"pass '-' as the argument","https_url":"certificates only — an https:// URL with --type receipt is treated as a file path and fails"},"dependencies":"none"},"proves":["a specific agent was authorized under a named policy (policy_hash, policy_version) to spend a specific amount on a specific rail","the charge reached a terminal settlement state (settlement_state, settled_at, external_payment_intent_id, external_charge_id)","when artifact_hash is present, precisely which artifact the payment was for"],"does_not_prove":["that the artifact was delivered","that the buyer received it"],"integrity_notes":{"when_present":"the issuer is stating that a binding this receipt would normally carry is absent, and why","when_absent":"the pre-signature gate found nothing to declare — NOT that no check ran","before_the_gate_existed":"receipts signed earlier carry neither the notes nor the bindings, and are annotated by separate append-only records rather than edited. Receipts are immutable; corrections are new records."},"self_attestation_caveat":"This specification, and any verifier downloaded from certifieddata.io, came from CertifiedData. Running our code against our server still trusts us. The mitigation is that the reference implementation is roughly ninety lines with no dependencies and can be read in full, obtained independently from GitHub, or discarded in favour of an implementation written from this specification."}