# Postcept Receipt: open verification standard **Version 2. Status: stable.** A Postcept Receipt is a signed, tamper-evident proof that a high-risk AI-agent action (a refund, a cancellation, a ticket resolution) was checked against the system of record and classified. This document specifies the receipt format and how to verify it without going through Postcept: no API call, no trust required. The point is neutrality. A customer, an auditor, or a counterparty can verify a receipt with nothing but the public key and the rules below. ## 1. Signature scheme - **Algorithm:** Ed25519 (`algorithm: "ed25519"`). - **Signature:** base64-encoded raw 64-byte Ed25519 signature, in the `signature` field. - **Public key:** base64-encoded raw 32-byte Ed25519 public key. Fetch the current key from `GET /v1/signing-key`, which returns `{ algorithm, key_id, public_key }`. - **Key id:** `ed25519:`, in `signing_key_id`. A receipt names the key that signed it, so old receipts stay verifiable after a key rotation. ## 2. Canonicalization The signature covers a deterministic encoding of the **signing body** (§3), not the receipt JSON as transmitted. To reproduce the signed bytes: 1. Build the signing body object for the receipt's `version` (§3). 2. Encode it as **canonical JSON**: - object keys sorted lexicographically (by UTF-16 code unit) - no insignificant whitespace (separators `,` and `:`) - every non-ASCII character escaped as `\uXXXX` (i.e. `ensure_ascii`) - numbers as their shortest round-trip form, integers without a decimal point. 3. UTF-8 encode the resulting string. Those are the signed bytes. This is what Python's `json.dumps(body, sort_keys=True, separators=(",", ":"))` produces. ### Timestamps Timestamps are ISO-8601 UTC. The signer emits a trailing `Z` (`2026-06-26T13:24:54.847945Z`). Some JSON serializers re-spell UTC as `+00:00`, so a verifier SHOULD try both spellings and accept the first that verifies. ## 3. Signing body ### Version 2 (current) Signs the full evidence, the tenant, the real connector identity, and the test flag. Neither the evidence nor the provenance can be altered without breaking the signature, and a sandbox receipt can't be passed off as a live one. ```jsonc { "version": "2", "id": "", "org_id": "", "operation_id": "", "agent_id": "", "action": "", "connectors_checked": ["stripe"], "test": false, "postconditions": [ { "name": "...", "category": "...|null", "status": "passed|failed|skipped", "expected": "...|null", "actual": "...|null", }, ], "result": "", "issued_at": "", "valid_as_of": "", } ``` ### Version 1 (legacy) Predates the full-evidence body. Postconditions carry only `name` and `status`, and `version`, `org_id`, `test`, `valid_as_of` are absent. A receipt with no `version` field is treated as version 1. ```jsonc { "id": "", "operation_id": "", "agent_id": "", "action": "", "connectors_checked": ["..."], "postconditions": [{ "name": "...", "status": "..." }], "result": "", "issued_at": "", } ``` ## 4. Verification algorithm ``` 1. Select the signing body by `version` (absent means "1"). 2. Canonicalize it (§2). 3. For each timestamp spelling, Ed25519-verify `signature` over the encoded body with the published public key. 4. The receipt is valid if any spelling verifies. ``` The reference is `verifyReceipt(receipt, publicKeyB64)` in this package. The Postcept API verifies with the same body in Python, and the two are kept byte-identical. ## 5. VCR-audit badge A free-audit badge (`verifyBadge`) uses the same scheme over a separate, PII-free body. The Verified Completion Rate is signed as an integer in basis points (`verified_completion_rate_bps`, 0 to 10000), not a float, so it canonicalizes the same way across languages even at whole-number rates. ```jsonc { "type": "postcept-vcr-audit", "label": "", "account_ref": "", "connector": "stripe:", "sampled": 25, "verified_completion_rate_bps": 9434, "issued_at": "", } ``` ## 6. Machine-readable schema A JSON Schema for the receipt object ships at `@postcept/receipt/schema` (`schema/receipt.schema.json`). It validates shape only. §2 to §4 govern the signature. ## 7. Transparency log Live receipts are appended to a public, append-only Merkle log (RFC 6962). It gives an independent timestamp: anyone can show that a receipt was logged and was not removed or back-dated, without taking Postcept's word for it. - **Leaf hash** (hex): `SHA-256(0x00 || receipt_id || 0x0a || signature)`, where `signature` is the base64 string from the receipt. Reconstructible from the receipt alone. - **Node hash:** `SHA-256(0x01 || left || right)`. - **Merkle root:** RFC 6962 Merkle Tree Hash over the leaf hashes in log order. - **Signed tree head (STH):** an Ed25519 signature (§1) over the canonical `{ "type": "postcept-sth", "tree_size", "root_hash", "timestamp" }`. Served at `GET /v1/transparency/sth`. - **Inclusion proof:** `{ receipt_id, leaf_index, leaf_hash, tree_size, audit_path [hex, deepest sibling first], sth }`. Served at `GET /v1/transparency/proof/{receipt_id}`. Verify it by recomputing the root from the leaf and audit path, comparing to the STH `root_hash`, then checking the STH signature. - **Consistency proof:** `{ first_size, second_size, first_root, second_root, proof [hex], sth }`. Served at `GET /v1/transparency/consistency?first={m}&second={n}` (`second` defaults to the current tree size). It proves the log at `second_size` is an append-only extension of the log at `first_size`. The earlier leaves are unchanged, and nothing was removed, reordered, or back-dated (RFC 6962 §2.1.2). A holder of an earlier signed tree head fetches a proof against it: verify the proof connects the two roots, then check that `first_root` equals the root they remembered and that the returned head is validly signed. Without this, inclusion proofs alone only bind a receipt to *whatever* root the operator serves now. The consistency proof is what makes "append-only" checkable rather than trusted. Reference: `verifyReceiptInLog`, `verifyInclusion`, `verifyConsistency`, `verifySignedTreeHead`, `receiptLeafHash` in this package.