Reference /Receipt schema

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.

What a receipt proves

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.

receipt.jsonjson
{
  "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_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 it reads {"_sd": "<commitment>"} in fields, and a matching marker plus merkle_root appear in redaction.
  • trust_level is L1 because 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 seq and prev_receipt_hash.
seqnumber
Optional. This receipt’s monotonic position within its session_id. Genesis is 0; each later receipt is exactly one greater. Present iff session_id is.
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 means action— 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.create or openai.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 coarse verb governs. Absent when the classifier produced no label.
target_hoststring
Optional. The destination host for http_request / payment and other external calls. Absent for llm_call and 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, or require_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. operator for the agent identity; approver for 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. destructive drops the value (it reads “[redacted]”); commit_and_reveal replaces 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_root commits to. See RedactionMarker below.
merkle_rootstring (64-hex)
Optional. BLAKE3 over the ordered commitments, present only in commit_and_reveal mode. Absent in destructive mode.

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_id or customer.ssn.
algorithmstringrequired
The commitment scheme. salted-blake3/v1 in commit_and_revealmode; a scheme-less tag the verifier reads as “no recoverable commitment” in destructive mode.
commitmentstringrequired
The commitment. BLAKE3(salt ++ field_path ++ value) as 64-hex in commit_and_reveal mode (the salt is sealed in a sidecar, never here); an empty string in destructivemode — 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 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 both action_hash and time_anchor removed). 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.