Start free

Receipt schema

A field-by-field reference for the Action Receipt JSON — the signed envelope, its content, the action, the policy outcome, signatures, and the optional redaction and approval records. Every field name is snake_case, exactly as it appears on the wire and as the verifier reads it.

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. L0 is operator-signed; L1 adds 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_call and 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.
fields records what HESO saw

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, or require_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; in and not_in carry a string array; exists ignores 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. operator for the agent identity; approver for 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. destructive drops the value entirely; commit_and_reveal keeps 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_reveal mode.

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_reveal mode; an empty string in destructive mode — never omitted.
Markers must be well-formed

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.

receipt.json
{  "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_usd is 12500, so the gt 5000 condition matched and the rule routed to require_approval.
  • member_id is redacted in commit_and_reveal mode, so its commitment and a merkle_root are present.
  • trust_level is L1 because both the operator and approver signatures verify.