Receipt schema
A receipt is one JSON object. This is the field-by-field reference for it: the signed envelope, its content, the action, the policy outcome, the signatures, and the optional approval, redaction, and transparency records. Every field name is snake_case, exactly as it appears on the wire and as the verifier reads it.
Each object below maps to a type in the Rust core. The Python SDK, the Node addon, and the browser WASM 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.
A receipt proves you authorized an action under your policy — and, at L1, that a person approved it with a device-held key. It does not prove the action succeeded downstream. fields records the arguments the agent passed, not the outcome of the call.
For the concepts behind the format, read Action receipts. For trust levels read Trust levels; for the cross-receipt chain, Audit chain; for what happens when you verify one, Verdicts.
A full receipt
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 truncated for readability.
{
"alg": "heso-action/v2+ed25519",
"content": {
"action_version": "heso-action/2.0",
"captured_at": "2026-06-06T14:22:09Z",
"agent_identity": "uP3kP9c0Yx2bQ7m1vWqJ4tR8nA6sF1bH0dGc9eL2b1=",
"action": {
"verb": "payment",
"tool_name": "stripe.transfers.create",
"domain": "payment",
"target_host": "api.stripe.com",
"workflow": "vendor-payouts",
"account": "acct_19",
"fields": {
"amount_usd": "12500",
"payee": "Globex LLC",
"member_id": { "_sd": "b9e0f72d4c1a8855e3f7b2906c1d4480a7e2f33915c8b0d6e441fa2c7b9e0f72" }
},
"result_hash": "7c41a2f09ab2…"
},
"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": "mK7c4Yx2bQ7m1vWqJ4tR8nA6sF1bH0dGc9eL2b1Qp0=",
"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": "member_id",
"algorithm": "salted-blake3/v1",
"commitment": "b9e0f72d4c1a8855e3f7b2906c1d4480a7e2f33915c8b0d6e441fa2c7b9e0f72"
}
],
"merkle_root": "1f88a330…"
},
"trust_level": "L1",
"action_hash": "9f2c…e1c0"
},
"signatures": [
{
"algorithm": "Ed25519",
"key_id": "operator",
"public_key": "uP3kP9c0Yx2bQ7m1vWqJ4tR8nA6sF1bH0dGc9eL2b1=",
"signature": "3a9f04af…"
},
{
"algorithm": "Ed25519",
"key_id": "approver",
"public_key": "mK7c4Yx2bQ7m1vWqJ4tR8nA6sF1bH0dGc9eL2b1Qp0=",
"signature": "d7105b2e…"
}
]
}amount_usdis12500, so thegt 5000condition matched and the rule routed torequire_approval.member_idis redacted incommit_and_revealmode, so it reads{"_sd": "<commitment>"}infields, and a matching marker plusmerkle_rootappear inredaction.trust_levelisL1because both the operator and approver signatures verify.
Top level: ActionReceipt
The envelope. It carries the algorithm tag, the signed content, the signatures over that content, and an optional transparency section. An unknown top-level key fails verification as Malformed— a receipt is exactly its signed content.
- alg"heso-action/v2+ed25519"required
- The envelope algorithm tag. The verifier checks this first; an unrecognized value fails with WrongAlgorithm.
- contentActionContentrequired
- The signed body — everything the signatures cover. See content below.
- signaturesSignatureEntry[]required
- One or more Ed25519 signatures over the canonical content bytes. The operator entry is always present; an approver entry is present at L1. See signatures.
- transparencyTransparencyProof[]
- Optional. RFC-6962 Merkle inclusion proofs from the transparency log. Lives outside
content, so a proof can be attached to a signed receipt without re-signing. Absent when empty. See transparency.
content (ActionContent)
The signed body. Its bytes are canonicalized with RFC-8785 (JCS) after the top-level action_hash field is removed. That canonical form is what every signature covers, and what action_hash is the BLAKE3 digest of. An unknown content key also fails as Malformed.
- action_version"heso-action/2.0"required
- The schema version. An unrecognized value fails with Unsupported.
- captured_atstring (RFC-3339)required
- UTC instant from the operator clock at capture time. Informational only — not a trusted timestamp. For trusted time, see time_anchor.
- agent_identitystring (base64)required
- The operator’s 32-byte Ed25519 public key, standard-alphabet base64. A display mirror of the
operatorsignature entry’s key — the verifier trusts the signature entry, not this field. - actionActionDetailrequired
- What the agent did. See action below.
- policyPolicyOutcomerequired
- The rule that matched and the decision it produced. See policy.
- approver_decisionApproverRecord
- Optional. Present on a single-approver L1receipt — the one human who co-signed. See approver_decision.
- redactionRedactionRecord
- Optional. Present when fields were redacted before signing. See redaction.
- trust_level"L0" | "L1"required
- The claimed trust level. The verifier re-derives this from the signatures that pass and ignores the embedded value; a mismatch fails with TrustLevelMismatch. There is no L2 or L3.
- action_hashstring (64-hex)required
- BLAKE3 of the canonical content, lowercase 64-hex. Stripped from the content before hashing, so it never hashes itself. The verifier recomputes and compares; a difference fails with HashMismatch.
- session_idstring
- Optional. The session a chained receipt belongs to. Present on every link of an audit chain; absent on a standalone receipt. Present together with
seqandprev_receipt_hash. - seqnumber
- Optional. This receipt’s monotonic position within its
session_id. Genesis is0; each later receipt is exactly one greater. Present iffsession_idis. - prev_receipt_hashstring (64-hex)
- Optional. BLAKE3 link to the predecessor receipt. Empty for genesis (
seq == 0); the previous link’s digest otherwise. It is signed content, so a receipt cannot be re-pointed at a different predecessor without breaking the operator signature. - kindReceiptKind
- Optional. The receipt’s role in the suspend/resume lifecycle (
action,suspended,approved,denied,expired,escalated,completed,key_rotation). Absent on the wire meansaction— the standalone path. See Human approval. - time_anchorTimeAnchor
- Optional. An RFC-3161 trusted-time anchor over
action_hash— an “existed no later than” bound, not when a human decided. Absent by default. A present anchor that does not verify fails with TimeAnchorUnverifiable. See time_anchor.
action (ActionDetail)
The captured action. The verb is one of seven values: llm_call, tool_call, http_request, payment, data_export, account_change, and delete. The verb is the authoritative signed lane every policy decision keys on.
- verbVerbrequired
- The kind of action. One of
llm_call,tool_call,http_request,payment,data_export,account_change,delete. - tool_namestringrequired
- The tool, function, or model invoked, e.g.
stripe.transfers.createoropenai.chat.completions. - fieldsRecord<string, JSON>required
- The action arguments, post-redaction. A destructive-redacted field reads
“[redacted]”; a commit-and-reveal field reads{"_sd": "<commitment>"}. Values are arbitrary JSON, not just strings. - domainstring
- Optional. A descriptive fine-grained lane label from the policy catalog (e.g.
payment,data_movement). The verifier never trusts it — the coarseverbgoverns. Absent when the classifier produced no label. - target_hoststring
- Optional. The destination host for
http_request/paymentand other external calls. Absent forllm_calland internal actions. - workflowstringrequired
- The workflow this action belongs to — the grouping key an auditor reconstructs a run from.
- accountstringrequired
- The account, tenant, or principal the action ran on behalf of.
- result_hashstring (64-hex)
- Optional. BLAKE3 of the action result, when one was bound. Absent when no result was recorded.
- errorstring
- Optional. The error the action raised, when it failed. Absent on success.
policy (PolicyOutcome)
The verdict from the policy engine: which rule matched, its plain-English sentence, the conditions that matched, and the decision taken. The decision_path is one of four values: allow, block, redact, and require_approval. See Policy.
- rule_idstringrequired
- The id of the matched rule in
heso.toml, or the synthetic default-rule id when nothing matched. - rule_displaystringrequired
- The natural-language sentence for the rule, e.g.
Require approval to pay over $5,000. Carried for display. - matched_conditionsMatchedCondition[]required
- The conditions on the rule that evaluated true for this action. See MatchedCondition below.
- decision_pathGateDecisionrequired
- The decision the engine took:
allow,block,redact, orrequire_approval.
MatchedCondition
Each entry of matched_conditions records one (field, op, value) triple the rule tested. See Conditions and operators.
- fieldstringrequired
- The action field the condition tested, e.g.
amount_usd. - opstringrequired
- The operator that was evaluated. One of
gt,lt,eq,contains,regex. - valueJSONrequired
- The comparison value. Numeric for
gt/lt, a string for the rest.
signatures (SignatureEntry[])
Each entry in the signaturesarray. The operator entry is always present; an approver entry is present at L1. The operator signs the action domain; the approver signs a distinct approval domain over the same canonical bytes — distinct domains stop a signature being transplanted between roles. The approver signs with their own device-held key; the cloud holds no signing key. See Trust levels.
- 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_keystring (base64)required
- The signing public key, standard-alphabet base64. The verifier checks the signature against this key.
- signaturestring (base64)required
- The Ed25519 signature over the role’s domain-prefixed canonical content bytes, base64. A bad operator or approver signature fails with InvalidSignature (or SelfApprovalif the approver key equals the operator’s).
approver_decision (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_identitystring (base64)required
- The approver's 32-byte Ed25519 public key, standard-alphabet base64. A display mirror of the approver signature entry's key.
- reasonstringrequired
- The reason the approver gave for the decision.
- decided_atstring (RFC-3339)required
- UTC instant the approver decided. Informational, like
captured_at— bound only by the approver’s own co-signature, never certified by a TSA. - sla_minutesnumber
- Optional. The service-level window, in minutes, the decision was expected within.
redaction (RedactionRecord)
Present in content.redaction when fields were redacted before signing, so plaintext never enters the signed bytes. The mode is one of two values: destructive or commit_and_reveal. See Redaction.
- mode"destructive" | "commit_and_reveal"required
- The redaction strategy.
destructivedrops the value (it reads“[redacted]”);commit_and_revealreplaces it with a salted BLAKE3 commitment so the value can be revealed and checked later. - markersRedactionMarker[]required
- One marker per redacted field, in the stable order
merkle_rootcommits to. See RedactionMarker below. - merkle_rootstring (64-hex)
- Optional. BLAKE3 over the ordered commitments, present only in
commit_and_revealmode. Absent indestructivemode.
RedactionMarker
Each entry of markers records what was redacted without revealing the value.
- field_pathstringrequired
- The dotted path of the redacted field within
action.fields, e.g.member_idorcustomer.ssn. - algorithmstringrequired
- The commitment scheme.
salted-blake3/v1incommit_and_revealmode; a scheme-less tag the verifier reads as “no recoverable commitment” indestructivemode. - commitmentstringrequired
- The commitment.
BLAKE3(salt ++ field_path ++ value)as 64-hex incommit_and_revealmode (the salt is sealed in a sidecar, never here); 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 MalformedRedaction.
transparency (TransparencyProof[])
Optional RFC-6962 Merkle inclusion proofs, stapled at export. They live outside content, so attaching one never invalidates a signature. The fields below cover a single-tree proof; HESO’s two-stage per-org log adds optional stage-2 fields (org_root, epoch, top_leaf_index, top_inclusion_proof). See Transparency log.
- log_idstringrequired
- The log this receipt was included in, e.g.
log.heso.ca. - leaf_indexnumberrequired
- The receipt's 0-based org-local leaf index — its position in the org's leaf sequence, which the stage-1 proof verifies against.
- inclusion_proofstring[]required
- The RFC-6962 inclusion proof: ordered sibling hashes (base64) from the leaf up to the org root.
- checkpointstringrequired
- The signed log checkpoint (C2SP signed-note form) over the root the proof is against. Byte-identical for every receipt of an epoch.
time_anchor (TimeAnchor)
Optional. An RFC-3161 trusted-time anchor over action_hash. Off by default. When present, the verifier checks it fail-closed; a present anchor that does not verify fails the whole receipt with TimeAnchorUnverifiable.
- kind"rfc3161"required
- The timestamp scheme. The only supported value in v2; any other is refused.
- token_b64string (base64)required
- The DER-encoded RFC-3161 Time-Stamp Token (the CMS SignedData), base64.
- tsastringrequired
- The TSA's advertised name or URL, informational. The trust decision is made against pinned roots, not this string.
- anchored_hashstring (64-hex)required
- The hash the TSA certifies — the pre-anchor
action_hash(canonical bytes with bothaction_hashandtime_anchorremoved). The verifier recomputes it and requires the token to certify the same value.
Verifying a receipt
To check a receipt you recompute action_hash over the RFC-8785 canonical content, verify each Ed25519 signature under its domain, confirm the redaction markers are well-formed, and re-derive the trust level from the signatures that passed. The full procedure and every failure status are on the Verdicts page; the model behind it is in Offline verification.