The Action Receipt is the unit of HESO: one JSON object that records exactly one action, the policy verdict that gated it, who approved it (if anyone), and a hash that locks the bytes in place. This page documents every type in that object. For the concepts behind it — what a receipt proves, and what it does not — read Action receipts. For what happens when you verify one, read Verdicts.
The shapes here are the single source of truth in the Rust core. The Python SDK, the Node addon, and the browser WASM all read the same structure, so a receipt made on one runtime verifies byte-for-byte on another. There is no separate JS or Python definition that can drift out of sync.
ActionReceipt
The top-level envelope. It carries the algorithm tag, the signed content, the signatures over that content, and an optional transparency section with inclusion and consistency proofs.
- alg"heso-action/v2+ed25519"required
- The algorithm identifier. The verifier checks this first; an unrecognized value fails with wrong_algorithm.
- contentActionContentrequired
- The signed body — everything the signatures cover. Documented in ActionContent below.
- signaturesSignatureEntry[]required
- One or more Ed25519 signatures over the canonical content bytes. At least the operator signature; the approver signature is present at L1. See SignatureEntry.
- transparencyunknown[]
- Optional. Inclusion and consistency proofs from the RFC-6962 Merkle transparency log (SHA-256). Absent when the receipt has not been logged.
ActionContent
The signed body. Its bytes are put in canonical form with RFC-8785 (JCS), with action_hash removed before hashing. That canonical form is what every signature covers, and what action_hash is the BLAKE3 digest of.
- action_version"heso-action/2.0"required
- The receipt schema version. An unrecognized value fails verification with unsupported_version.
- captured_atstring (ISO)required
- ISO-8601 timestamp recorded when the action was captured, before the policy decision.
- agent_identitystringrequired
- The operator public key, base64-encoded — the identity that signs the receipt.
- actionActionDetailrequired
- What the agent did. See ActionDetail below.
- policyPolicyOutcomerequired
- The rule that matched and the path it took. See PolicyOutcome.
- approver_decisionApproverRecord
- Optional. Present when the action was routed to a human. See ApproverRecord.
- redactionRedactionRecord
- Optional. Present when fields were redacted before signing. See RedactionRecord.
- trust_level"L0" | "L1"required
- The claimed trust level.
L0is operator-signed;L1adds a human approver co-signature. The verifier re-derives this from which signatures pass; a mismatch fails with trust_mismatch. There is no L2 or L3. - action_hashstringrequired
- The BLAKE3 digest of the canonical content bytes, lowercase 64-hex. The verifier recomputes it and compares; a difference fails with hash_mismatch. It is stripped from the content before hashing, so it never hashes itself.
ActionDetail
The captured action itself. The verb is one of seven values: llm_call, tool_call, http_request, payment, data_export, account_change, and delete.
- verbVerbrequired
- The kind of action. One of
llm_call,tool_call,http_request,payment,data_export,account_change,delete. - tool_namestringrequired
- The tool or method invoked, e.g. stripe.transfers.create.
- target_hoststring
- Optional. The destination host. Omitted for
llm_calland other internal actions that do not leave a host. - workflowstringrequired
- The workflow this action belongs to — the unit a policy subject can scope to.
- accountstringrequired
- The account the action ran under.
- fieldsRecord<string, string>required
- The action arguments, as a string map. These are post-redaction: any field listed in a redaction marker has already been removed or replaced before signing.
- result_hashstring
- Optional. A hash of the action result, when one was captured.
- errorstring
- Optional. The error, when the action failed.
The receipt records the arguments the agent passed and the policy that gated them, then signs them. It proves the operator authorized this call under a known rule — and, at L1, that a person approved it with a device-held key. It captures the inputs as the agent passed them, not whether the call succeeded downstream.
PolicyOutcome
The verdict from the policy engine: which rule matched, the plain-English sentence for it, the conditions that matched, and the path taken. The decision_path is one of four values: allow, block, redact, and require_approval (route-to-human).
- rule_idstringrequired
- The id of the matched rule in
heso.toml. - rule_displaystringrequired
- The natural-language sentence for the rule, e.g. “Require approval to pay over $5,000”.
- matched_conditionsMatchedCondition[]required
- The conditions on the rule that evaluated true for this action. See MatchedCondition just below.
- decision_pathDecisionPathrequired
- The path the engine took:
allow,block,redact, orrequire_approval.
Each entry of matched_conditions is a MatchedCondition:
- fieldstringrequired
- The action field the condition tested, e.g. amount_usd.
- opConditionOprequired
- The operator that was evaluated. One of
gt,lt,gte,lte,eq,neq,in,not_in,exists,matches. See Conditions and operators. - valueJSONrequired
- The typed comparison value. Numeric ops carry a number;
inandnot_incarry a string array;existsignores its value.
SignatureEntry
Each signature in the signatures array. The operator entry is always present; the approver entry is present at L1. The approver signs with their own device-held key — the cloud holds no signing key. The key_id is one of two values: operator or approver.
- algorithm"Ed25519"required
- The signature algorithm. Always Ed25519.
- key_id"operator" | "approver"required
- Which key produced this signature.
operatorfor the agent identity;approverfor the human at L1. - public_keystringrequired
- The signing public key, base64-encoded. The verifier checks the signature against this key.
- signaturestringrequired
- The Ed25519 signature over the canonical content bytes, base64-encoded. A bad operator signature fails with invalid_signature; a bad approver signature fails with invalid_approver.
ApproverRecord
Present in content.approver_decision when an action was routed to a human. The decision is one of approved, rejected, or escalated.
- decision"approved" | "rejected" | "escalated"required
- What the human decided. See Human approval for the routing flow.
- approver_identitystringrequired
- The approver public key, base64-encoded — the same key that produced the approver signature.
- reasonstringrequired
- The reason the approver gave for the decision.
- decided_atstring (ISO)required
- ISO-8601 timestamp of when the decision was made.
- sla_minutesnumber
- Optional. The service-level window, in minutes, the decision was expected within.
RedactionRecord
Present in content.redaction when fields were redacted before signing — so secrets never reach our servers. The mode is one of two values: destructive or commit_and_reveal.
- mode"destructive" | "commit_and_reveal"required
- The redaction strategy.
destructivedrops the value entirely;commit_and_revealkeeps a hash commitment so the value can be revealed and checked later. See Redaction. - markersRedactionMarker[]required
- One marker per redacted field. See RedactionMarker below.
- merkle_rootstring
- Optional. The Merkle root over the commitments. Present only in
commit_and_revealmode.
Each entry of markers is a RedactionMarker:
- field_pathstringrequired
- The path to the redacted field, e.g.
action.fields.member_id. - algorithm"blake3"required
- The commitment hash algorithm. Always BLAKE3.
- commitmentstringrequired
- The hash commitment. A 64-hex digest in
commit_and_revealmode; an empty string indestructivemode — never omitted.
The verifier checks that redaction markers are well-formed before it trusts the trust level. A malformed marker fails with redaction_malformed.
Full example
A complete L1 payment receipt. It went over the approval cap, was routed to a human and approved, and had one field redacted with a commitment before signing. It carries both the operator and approver signatures. Hashes, keys, and signatures are shortened with an ellipsis.
{ "alg": "heso-action/v2+ed25519", "content": { "action_version": "heso-action/2.0", "captured_at": "2026-06-06T14:22:09Z", "agent_identity": "ed25519:uP3…b1", "action": { "verb": "payment", "tool_name": "stripe.transfers.create", "target_host": "api.stripe.com", "workflow": "vendor-payouts", "account": "acct_19", "fields": { "amount_usd": "12500", "payee": "Globex LLC", "member_id": "[redacted]" }, "result_hash": "7c41…9ab2" }, "policy": { "rule_id": "pay-cap", "rule_display": "Require approval to pay over $5,000", "matched_conditions": [ { "field": "amount_usd", "op": "gt", "value": 5000 } ], "decision_path": "require_approval" }, "approver_decision": { "decision": "approved", "approver_identity": "ed25519:mK7…c4", "reason": "Verified invoice INV-2207 against the PO.", "decided_at": "2026-06-06T14:25:41Z", "sla_minutes": 60 }, "redaction": { "mode": "commit_and_reveal", "markers": [ { "field_path": "action.fields.member_id", "algorithm": "blake3", "commitment": "b9e0…f72d" } ], "merkle_root": "1f88…a330" }, "trust_level": "L1", "action_hash": "9f2c…e1c0" }, "signatures": [ { "algorithm": "Ed25519", "key_id": "operator", "public_key": "ed25519:uP3…b1", "signature": "3a9f…04af" }, { "algorithm": "Ed25519", "key_id": "approver", "public_key": "ed25519:mK7…c4", "signature": "d710…5b2e" } ]}
To check this receipt, you recompute action_hash over the canonical content, verify both Ed25519 signatures, confirm the redaction markers are well-formed, and re-derive the trust level from the signatures that passed. The full step-by-step procedure is on the Verdicts page; the model behind it is in Offline verification.
amount_usdis12500, so thegt 5000condition matched and the rule routed torequire_approval.member_idis redacted incommit_and_revealmode, so its commitment and amerkle_rootare present.trust_levelisL1because both the operator and approver signatures verify.
