Skip to content
/docs

Receipts API

Export and verify canonical decision receipts.

Every stored Veto decision can produce a canonical veto.receipt/1 payload. Receipts are deterministic JSON payloads with stable canonical hashes (RFC 8785 JCS over SHA-256), a hash link to the previous receipt, and a protocol root field, so anyone can replay the chain and detect hash-link tampering offline without calling Veto.

The same protocol shape is exposed everywhere a decision is recorded:

  • POST /v1/validate returns a compact receipt summary on each recorded decision. (The SDK snake_case endpoint POST /v1/tools/validate records the decision but does not return a receipt inline.)
  • Guarded MCP tool calls record receipts and surface summaries on the decision event (deny / require_approval) or the response _meta (allow).
  • Resolving an approval appends a chained approval_approved / approval_denied receipt.
  • Decisions list and detail responses include the summary; GET /v1/decisions/:id/receipt returns the full payload.
  • GET /v1/receipts/export streams full payloads as NDJSON for an audit archive, SIEM pipeline, or offline verifier.

The canonical schema is published by the veto-receipt-protocol package and mirrored in the server OpenAPI document (GET /v1/openapi.json) as DecisionReceiptPayload and ReceiptSummary. See Decision receipts for the model and Verify decision receipts for the end-to-end workflow.

Receipt summary

{
  "receipt_id": "rcp_000000000000000000000001",
  "receipt_hash": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
  "previous_receipt_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "merkle_root": "sha256:2222222222222222222222222222222222222222222222222222222222222222"
}

Receipt payload

{
  "version": "veto.receipt/1",
  "receipt_id": "rcp_000000000000000000000001",
  "organization_id": "org_123",
  "project_id": "proj_123",
  "decision_id": "d7f2a1b3-4c5e-6f78-9a0b-1c2d3e4f5a6b",
  "approval_id": null,
  "session_id": "sess_abc123",
  "agent_id": "refund-agent",
  "client_id": null,
  "connection_id": null,
  "upstream_id": null,
  "tool_name": "issue_refund",
  "tool_schema_hash": null,
  "policy_id": "pol_123",
  "policy_version": "7",
  "policy_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
  "decision": "deny",
  "reason_code": "amount_exceeds_limit",
  "reason_detail": "amount_usd exceeds the configured policy limit",
  "redacted_arguments": { "amount_usd": 900, "customer_email": "[redacted]" },
  "argument_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444",
  "result_hash": null,
  "approval_hash": null,
  "previous_receipt_hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "merkle_root": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
  "timestamp": "2026-06-02T12:00:00Z",
  "trace_id": "trace_abc123"
}

The first receipt in a chain uses the SHA-256 hash of the empty byte string (sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855) as previous_receipt_hash.

Receipt fields

FieldTypeDescription
version"veto.receipt/1"Protocol version literal
receipt_idstringStable receipt identifier (rcp_…)
organization_idstringOwning organization
project_idstring | nullOwning project, when scoped
decision_idstringDecision this receipt attests
approval_idstring | nullLinked approval for HITL receipts
session_idstring | nullAgent session, when provided
agent_idstring | nullAgent identity from decision context
client_id / connection_id / upstream_idstring | nullMCP gateway client, connection, and upstream binding
tool_namestringTool the decision governs
tool_schema_hashstring | nullCanonical hash of the tool schema, when known
policy_idstring | nullPolicy that produced the decision
policy_versionstringPolicy version string
policy_hashstringCanonical hash of the evaluated policy
decisionenumallow, deny, require_approval, approval_approved, or approval_denied
reason_codestring | nullMachine-stable reason slug
reason_detailstring | nullHuman-readable reason
redacted_argumentsobjectArguments with string values redacted to [redacted]
argument_hashstringCanonical hash of the raw (un-redacted) arguments
result_hashstring | nullCanonical hash of the tool result, bound after execution (MCP allow path)
approval_hashstring | nullCanonical hash of the approval resolution
previous_receipt_hashstringHash link to the prior receipt in the chain
merkle_rootstringProtocol root carried by the receipt chain
timestampstringRFC 3339 issue time
trace_idstring | nullOptional distributed-trace correlation ID

Hashes are lowercase hex prefixed with sha256:. argument_hash is computed over the raw arguments while redacted_arguments strips string values, so a receipt proves which arguments were evaluated without persisting their plaintext.

GET /v1/decisions/:id/receipt

Fetch the canonical receipt payload for one decision.

Headers

HeaderRequiredDescription
X-Veto-API-Key or AuthorizationYesProject-scoped API key or Bearer JWT

Response

Returns the full veto.receipt/1 JSON payload, or 404 not_found when no receipt exists for the decision.

curl https://api.veto.so/v1/decisions/d7f2a1b3-4c5e-6f78-9a0b-1c2d3e4f5a6b/receipt \
  -H "X-Veto-API-Key: $VETO_API_KEY"

Dashboard (JWT) callers need the decision:read permission. Project-scoped API keys can only fetch receipts for their own project.

GET /v1/receipts/export

Export canonical receipt payloads as NDJSON.

Headers

HeaderRequiredDescription
X-Veto-API-Key or AuthorizationYesProject-scoped API key or Bearer JWT

Query parameters

ParameterTypeDefaultDescription
format"ndjson"ndjsonExport format. ndjson is the only supported value.
projectIdstringAPI key scopeFilter by project
startDateISO 8601 dateInclude receipts at or after this date
endDateISO 8601 dateInclude receipts at or before this date
cursorstringfirst pageOpaque continuation token from X-Veto-Next-Cursor
limitnumber10000Receipts per page, 1–10000

Dashboard (JWT) callers need the decision:read permission; project-scoped API keys are restricted to their own project.

For offline chain verification, export one project chain at a time. Organization-wide dashboard exports are useful for archive/search workflows, but they may contain multiple project-scoped chains.

Response

Content-Type: application/x-ndjson
Content-Disposition: attachment; filename=veto-receipts.ndjson
X-Veto-Next-Cursor: <opaque-token>

Each line is one full veto.receipt/1 JSON payload.

curl "https://api.veto.so/v1/receipts/export?format=ndjson&limit=1000" \
  -H "X-Veto-API-Key: $VETO_API_KEY" \
  -o receipts.ndjson

When X-Veto-Next-Cursor is present, more pages remain — request the next one with cursor=<value> and stop when the header is absent. On Veto Cloud the cursor is an opaque token; on the self-hosted local appliance it is a numeric offset. Treat it as opaque and echo it back verbatim either way.

Offline verification

Receipts are verifiable without calling Veto. Re-hash each payload, confirm every previous_receipt_hash links to the prior receipt, and validate timestamp/root-continuity rules. The TypeScript protocol package does this for you:

import { parseReceiptNdjson, verifyDecisionReceiptChain } from "veto-receipt-protocol";

const receipts = parseReceiptNdjson(ndjson);
const result = verifyDecisionReceiptChain(receipts);

if (!result.ok) {
  throw new Error(`receipt chain broke at ${result.breakAt}: ${result.reason}`);
}

See Verify decision receipts for a CI gate and SIEM ingestion walkthrough.