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/validatereturns a compact receipt summary on each recorded decision. (The SDK snake_case endpointPOST /v1/tools/validaterecords 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_deniedreceipt. - Decisions list and detail responses include the summary;
GET /v1/decisions/:id/receiptreturns the full payload. GET /v1/receipts/exportstreams 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
| Field | Type | Description |
|---|---|---|
version | "veto.receipt/1" | Protocol version literal |
receipt_id | string | Stable receipt identifier (rcp_…) |
organization_id | string | Owning organization |
project_id | string | null | Owning project, when scoped |
decision_id | string | Decision this receipt attests |
approval_id | string | null | Linked approval for HITL receipts |
session_id | string | null | Agent session, when provided |
agent_id | string | null | Agent identity from decision context |
client_id / connection_id / upstream_id | string | null | MCP gateway client, connection, and upstream binding |
tool_name | string | Tool the decision governs |
tool_schema_hash | string | null | Canonical hash of the tool schema, when known |
policy_id | string | null | Policy that produced the decision |
policy_version | string | Policy version string |
policy_hash | string | Canonical hash of the evaluated policy |
decision | enum | allow, deny, require_approval, approval_approved, or approval_denied |
reason_code | string | null | Machine-stable reason slug |
reason_detail | string | null | Human-readable reason |
redacted_arguments | object | Arguments with string values redacted to [redacted] |
argument_hash | string | Canonical hash of the raw (un-redacted) arguments |
result_hash | string | null | Canonical hash of the tool result, bound after execution (MCP allow path) |
approval_hash | string | null | Canonical hash of the approval resolution |
previous_receipt_hash | string | Hash link to the prior receipt in the chain |
merkle_root | string | Protocol root carried by the receipt chain |
timestamp | string | RFC 3339 issue time |
trace_id | string | null | Optional 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
| Header | Required | Description |
|---|---|---|
X-Veto-API-Key or Authorization | Yes | Project-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
| Header | Required | Description |
|---|---|---|
X-Veto-API-Key or Authorization | Yes | Project-scoped API key or Bearer JWT |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
format | "ndjson" | ndjson | Export format. ndjson is the only supported value. |
projectId | string | API key scope | Filter by project |
startDate | ISO 8601 date | Include receipts at or after this date | |
endDate | ISO 8601 date | Include receipts at or before this date | |
cursor | string | first page | Opaque continuation token from X-Veto-Next-Cursor |
limit | number | 10000 | Receipts 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.ndjsonWhen 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.

