Receipts
A receipt is the proof an action happened. Self-contained, hash-chained, signed. Anyone with the published public key can verify it offline. The verifier never needs to contact the issuer — that's the whole point.
Format
{
"version": { "spec": "ep-receipt/2026-04-27" },
"receiptId": "365dd91c-9bed-406c-962b-963451de90d1",
"transactionId": "txn_hotel_executed",
"agentId": "agent_sandbox_demo",
"sessionId": "session_sandbox",
"created": "2026-05-06T05:40:24.558Z",
"entries": [
{
"entryId": "…",
"index": 0,
"stepName": "__genesis__",
"previousHash": "0000000000000000000000000000000000000000000000000000000000000000",
"hash": "2881b9b85c158328…",
"input": null,
"output": null,
"startTime": "…",
"endTime": "…",
"latencyMs": 0,
"cost": null,
"error": null,
"metadata": {}
}
// … 8 more entries: token_validation → schema → boundary → completeness →
// math → execute → commit_auth_split → receipt, each chained by hash.
],
"signature": {
"kid": "sandbox-2026-05",
"alg": "ES256",
"value": "p2bTEu-Zhos9SpmRIz-9Iaad4oUx3d7DkFLc6wTMCw63bHKDoefQxk8FEnZAkC4Zlpzj5pdJ5UhpgrzyDlzxpA"
},
"amendmentOf": null,
"eventType": "ORIGINAL",
"paymentStatus": "charged",
"money": {
"offerCurrency": "USD",
"offerAmount": "165.52",
"chargeCurrency": "USD",
"chargeAmount": "165.52"
},
"idempotencyKey": "idem_hotel_executed",
"replicaId": "sandbox-replica-0",
"chainId": "chain_hotel_executed",
"regulatoryFramework": [],
"metadata": { "sandbox": true }
}
{
"version": { "spec": "ep-receipt/2026-04-27" },
"receiptId": "365dd91c-9bed-406c-962b-963451de90d1",
"transactionId": "txn_hotel_executed",
"agentId": "agent_sandbox_demo",
"sessionId": "session_sandbox",
"created": "2026-05-06T05:40:24.558Z",
"entries": [
{
"entryId": "…",
"index": 0,
"stepName": "__genesis__",
"previousHash": "0000000000000000000000000000000000000000000000000000000000000000",
"hash": "2881b9b85c158328…",
"input": null,
"output": null,
"startTime": "…",
"endTime": "…",
"latencyMs": 0,
"cost": null,
"error": null,
"metadata": {}
}
// … 8 more entries: token_validation → schema → boundary → completeness →
// math → execute → commit_auth_split → receipt, each chained by hash.
],
"signature": {
"kid": "sandbox-2026-05",
"alg": "ES256",
"value": "p2bTEu-Zhos9SpmRIz-9Iaad4oUx3d7DkFLc6wTMCw63bHKDoefQxk8FEnZAkC4Zlpzj5pdJ5UhpgrzyDlzxpA"
},
"amendmentOf": null,
"eventType": "ORIGINAL",
"paymentStatus": "charged",
"money": {
"offerCurrency": "USD",
"offerAmount": "165.52",
"chargeCurrency": "USD",
"chargeAmount": "165.52"
},
"idempotencyKey": "idem_hotel_executed",
"replicaId": "sandbox-replica-0",
"chainId": "chain_hotel_executed",
"regulatoryFramework": [],
"metadata": { "sandbox": true }
}
Why self-contained
The signature is over the canonical RFC 8785 JCS bytes of the entire
receipt except signature.value. signature.kid and signature.alg
ARE in the hash — the signer commits to which key produced the signature.
The verifier needs nothing else. No database lookup, no API call, no shared secret. Just:
- The receipt JSON.
- The published JWK for the kid (fetched once from
/.well-known/jwks.json).
This matters because:
- Compliance auditors can verify receipts in their own infrastructure without network access to the issuer.
- Customers can independently confirm what an agent did, even years later, after the issuer is gone.
- Third parties (insurance, regulators) can validate without trusting the vendor.
Audit chain
Every receipt carries an entries[] array. Each entry is one stage of
the pipeline (genesis sentinel, then 8 canonical stages: token_validation,
schema, boundary, completeness, math, execute, commit_auth_split,
receipt). Each entry's hash covers all of its own fields plus the
previous entry's hash via previousHash. Genesis uses a sentinel of 64
hex zeros.
Tamper any single byte of any entry and the chain check breaks at exactly
that index. Tamper the receipt envelope (e.g. money.chargeAmount) and
the receipt-level signature check fails even when the chain is intact.
Blocked receipts are signed too
A blocked action emits a signed receipt with kind: 'blocked' and
paymentStatus: 'not_charged'. The entries[] chain shows exactly how
far the action got and which stage stopped it. The proof that the
gateway blocked the action is the receipt — not the absence of a
receipt.
Verify
Paste any receipt at /verify. The verifier runs four checks:
- Structural — hash chain intact (genesis + every prev/hash link).
- Kid resolved —
signature.kidmatches a key in/.well-known/jwks.json. - ES256 signature — ECDSA P-256 signature valid against the canonical receipt bytes.
- Not quarantined — the resolved key is
ep_status: activeand the receipt'screatedis within the key's active window.
A receipt is valid only if all four pass.
Failure modes
- Public key rotation — when the signing key rotates, old receipts
remain verifiable with the old kid. Both keys stay published in the
JWKS with
ep_status: 'verify-only'for the rotated one. - Algorithm migration — the receipt's
signature.algtag dispatches the verifier. Future algorithms can coexist; older receipts verify under the algorithm they were signed with. - Truncation — a receipt missing required fields fails canonicalisation and verification fails loudly. There is no partial verification.
See also
- How it works — the deterministic pipeline.
- Verify a receipt — standalone verifier.
- Errors — error shape.