Skip to content
/docs

Decision Receipts

How Veto turns every policy decision into a portable, hash-linked, offline-verifiable veto.receipt/1 record.

A decision receipt is the portable, tamper-evident proof of a single authorization decision. Every time Veto allows, denies, escalates, or resolves a tool call, it can emit a canonical veto.receipt/1 payload that binds what was asked, which policy decided, what was decided, and what happened next into one deterministic record that anyone can verify later — without calling Veto.

If policy evaluation answers "may this action run?", receipts answer the question auditors, incident responders, and other agents ask afterward: "prove what you authorized, and prove the record was not edited."

Why a receipt and not a log line

Application logs are mutable, vendor-specific, and trust the writer. That is the wrong foundation for autonomous agents that move money, change infrastructure, or touch regulated data. A decision receipt is different in three ways:

  • Canonical. The payload is serialized with RFC 8785 JSON Canonicalization and hashed with SHA-256, so the same decision always produces the same bytes and the same hash on any machine.
  • Chained. Each receipt carries previous_receipt_hash, linking it to the one before. Editing, reordering, or dropping a receipt breaks the chain.
  • Portable. veto.receipt/1 is a versioned, vendor-neutral schema. A receipt verifies the same way in your CI, your SIEM, a regulator's laptop, or another agent's runtime — using the open veto-receipt-protocol package, not a Veto API call.

What a receipt binds

Each receipt payload commits to the full context of one decision:

GroupFieldsWhat it proves
Subjectorganization_id, project_id, agent_id, session_id, client_id, connection_id, upstream_idWho acted, and through which gateway path
Actiontool_name, tool_schema_hash, redacted_arguments, argument_hashWhich tool was called, with which arguments
Authoritypolicy_id, policy_version, policy_hash, decision, reason_code, reason_detailWhich policy version decided, and why
Outcomeresult_hash, approval_id, approval_hashWhat the tool returned, and who approved it
Integrityprevious_receipt_hash, merkle_root, timestamp, trace_idWhere it sits in the tamper-evident chain

The decision field is one of allow, deny, require_approval, approval_approved, or approval_denied.

Redaction without losing provenance

Receipts must be safe to archive and forward, but still prove which arguments were evaluated. Veto stores two fields:

  • argument_hash — the canonical hash of the raw arguments.
  • redacted_arguments — the same structure with string values replaced by [redacted] (numbers, booleans, and nulls are preserved).

So a receipt for a $900 refund keeps { "amount_usd": 900, "customer_email": "[redacted]" } and a hash that still binds the exact email that was checked. You can prove an argument matched a known value by re-hashing it; you cannot read secrets out of the archive.

The chain and the Merkle root

Receipts form an append-only hash chain. The first receipt links to a fixed genesis value — the SHA-256 of the empty byte string (sha256:e3b0c4…b855) — and every later receipt links to its predecessor's hash. Each receipt also carries a protocol merkle_root field; the current verifier checks root continuity rather than claiming full batch-membership recomputation.

Verification re-derives all of this locally:

  1. Re-canonicalize and re-hash each payload; compare to the recorded hash.
  2. Confirm each previous_receipt_hash equals the prior receipt's hash.
  3. Check timestamp ordering and merkle_root continuity rules.

Any mutation — a changed amount, a swapped policy version, a deleted denial — changes a hash and breaks the chain at a known index.

The lifecycle of a receipt

Receipts track the whole decision, not just the first answer:

  • A POST /v1/validate call or a guarded MCP tool call writes a receipt for the allow / deny / require_approval decision and surfaces a compact summary.
  • On the MCP allow path the receipt is finalized after the upstream tool runs, binding a result_hash over the output.
  • Resolving an approval appends a second, chained receipt (approval_approved / approval_denied) that binds the approval_hash.

The result is a continuous chain where a human escalation and its resolution are cryptographically connected to the original request.

Summaries vs. canonical payloads

Hot-path responses return a four-field summary (receipt_id, receipt_hash, previous_receipt_hash, merkle_root) so latency stays low. The full canonical payload stays available for later:

  • GET /v1/decisions/:id/receipt — one canonical payload.
  • GET /v1/receipts/export?format=ndjson — a stream of payloads for archives, SIEM ingestion, or batch verification.

Where receipts fit

Receipts are the verifiable surface of Veto's broader authority model. The audit hash chain anchors tenant-wide Merkle roots; receipts are the portable, per-decision artifact you can hand to someone outside your trust boundary. They are tamper-evident by construction: verification detects any edit to an exported chain. For external anchoring (publishing a root so you can prove a receipt existed at a point in time), pair exports with your own timestamping or the platform's chain anchoring.

Next steps

Go toFor
Receipts APIEndpoints, payload schema, and field reference
Verify decision receiptsExport, verify in CI, and forward to a SIEM
SDK referenceveto-receipt-protocol helpers and JSON Schema exports