Action receipts
A receipt is the signed JSON record of one action: what it was, the policy verdict that gated it, who signed it, and a hash that binds it all together.
Every action your agent takes — an LLM call, a tool call, a payment, a data export — is captured, checked against policy, optionally routed to a human, signed, and emitted as one Action Receipt. The receipt is self-contained JSON. Anyone with the bytes can verify it offline, in any browser, with no HESO infrastructure.
A receipt proves you authorized an action under your policy. It does not prove the action succeeded downstream. HESO records authorization; the outcome is a separate question.
The envelope
A receipt has three top-level parts: the algorithm tag, the signed content, and the signatures over a hash of that content.
{
"alg": "heso-action/v2+ed25519",
"content": {
"action_version": "heso-action/2.0",
"captured_at": "2026-01-14T18:04:31Z",
"agent_identity": "ed25519:uP3…b1",
"action": {
"verb": "payment",
"tool_name": "stripe.transfers.create",
"fields": { "amount_usd": "8200", "payee": "Globex LLC" }
},
"policy": {
"rule_id": "pay-cap",
"decision": "require_approval",
"matched_conditions": [
{ "field": "amount_usd", "op": "gt", "value": 5000 }
]
},
"approver_decision": {
"decision": "approved",
"approver_identity": "ed25519:7Qm…9c",
"reason": "Confirmed invoice with finance.",
"decided_at": "2026-01-14T18:11:52Z"
},
"trust_level": "L1",
"action_hash": "9f2c…e1c0",
"session_id": "vendor-payouts",
"seq": 12,
"prev_receipt_hash": "4b8e…77a1"
},
"signatures": [
{
"algorithm": "Ed25519",
"key_id": "operator",
"public_key": "ed25519:uP3…b1",
"signature": "3a9f…04af"
},
{
"algorithm": "Ed25519",
"key_id": "approver",
"public_key": "ed25519:7Qm…9c",
"signature": "be20…7d11"
}
],
"transparency": []
}Hashes and keys are truncated with an ellipsis here for readability.
- alg"heso-action/v2+ed25519"required
- The format and signature scheme. The verifier rejects anything it does not recognize before any other check runs.
- contentobjectrequired
- The signed payload: the action, the policy verdict, the trust level, the
action_hash, and the audit-chain fields. Detailed below. - signaturesarrayrequired
- One or more Ed25519 signatures over the content hash. An
operatorentry is always present; anapproverentry is added when a human co-signs. - transparencyarray
- Optional RFC-6962 inclusion and consistency proofs from the transparency log. Empty until the receipt is logged.
Inside content
The content object is the part that gets hashed and signed. It records what the agent did and the verdict that gated it. On the wire every field name is snake_case.
- actionobjectrequired
- What was attempted: the verb, the tool name, and the captured
fieldsmap (always post-redaction — a redacted value never appears here in the clear). - policyobjectrequired
- The policy verdict: the rule that matched, the decision, and the conditions it matched on.
- trust_level"L0" | "L1"required
- Operator-only (
L0) or operator plus a human approver (L1). The verifier re-derives this from the signatures actually present. See trust levels. - action_hashstring (64-hex)required
- The BLAKE3 hash of the canonical content. Recomputing it is how tampering is detected. See below.
- session_id, seq, prev_receipt_hashstring · int · string
- The chain fields. Each receipt points at the one before it, linking a session into a tamper-evident ledger. See the audit chain.
- approver_decision, redaction, time_anchorobject (optional)
- Attached only when the action took those paths — a human approved it, fields were redacted, or a trusted-time anchor was bound. All are signed with everything else.
This is the conceptual tour, not the exhaustive schema. For every field, every type, and every optional value, see the receipt schema reference.
The action hash
The action_hash is the BLAKE3 hash of content, with the action_hash field itself removed before hashing. Change any field and it no longer matches.
The content is first serialized under RFC-8785 (JCS): keys sorted, whitespace fixed, numbers normalized, so the same content always produces the same bytes no matter who wrote the JSON. The verifier re-serializes, recomputes the BLAKE3 hash, and compares it to the embedded value. If one byte changed, the hashes differ and verification fails with HashMismatch.
The hash and canonicalization are computed by the Rust core. Python, Node, and the browser WASM all call that same core, so the recomputed action_hashis byte-identical everywhere — there is no separate JavaScript or Python crypto to drift.
The signatures
Each entry in signatures is one Ed25519 signature over the action_hash, tagged by key_id. At L0 there is a single operator entry. At L1 an approverentry is added, signed with the approver’s own device-held key. The cloud holds no signing key, so a co-signature is real proof that that person approved the action.
The verifier re-derives the trust level from which signatures actually pass: a receipt cannot claim L1 without a valid approver signature behind it. See trust levels for both shapes (single approver and k-of-n quorum) and the audit chain for how receipts link together.