Verify Decision Receipts
Export canonical veto.receipt/1 receipts and verify the chain offline, in CI, and into a SIEM.
This guide takes you from a running Veto deployment to a verified, archived receipt chain. Receipts are canonical, hash-linked records of every policy decision; verifying them proves your audit trail was not edited — without trusting Veto at read time.
You need a project-scoped VETO_API_KEY (or a dashboard JWT with decision:read) and the veto-receipt-protocol package.
1. Export receipts as NDJSON
Pull canonical payloads for a project. Each line is one full veto.receipt/1 record.
curl "https://api.veto.so/v1/receipts/export?format=ndjson&limit=1000" \
-H "X-Veto-API-Key: $VETO_API_KEY" \
-o receipts.ndjsonPaginate large windows
When the response includes an X-Veto-Next-Cursor header, more pages remain. Echo the token back verbatim until it disappears:
cursor=""
: > receipts.ndjson
while : ; do
page=$(curl -sS -D headers.txt \
"https://api.veto.so/v1/receipts/export?format=ndjson&limit=10000&cursor=${cursor}" \
-H "X-Veto-API-Key: $VETO_API_KEY")
printf '%s\n' "$page" >> receipts.ndjson
cursor=$(grep -i '^X-Veto-Next-Cursor:' headers.txt | awk '{print $2}' | tr -d '\r')
[ -z "$cursor" ] && break
done2. Verify the chain offline
Verification re-hashes every payload, checks each previous_receipt_hash link, and validates timestamp/root-continuity rules. Any edit breaks it at a known index.
import { readFileSync } from "node:fs";
import { parseReceiptNdjson, verifyDecisionReceiptChain } from "veto-receipt-protocol";
const receipts = parseReceiptNdjson(readFileSync("receipts.ndjson", "utf8"));
const result = verifyDecisionReceiptChain(receipts);
if (!result.ok) {
throw new Error(`receipt chain broke at index ${result.breakAt}: ${result.reason}`);
}
console.log(`verified ${receipts.length} receipts`);3. Gate a build on chain integrity
Fail CI if the exported chain does not verify. Example GitHub Actions step:
- name: Verify Veto receipt chain
env:
VETO_API_KEY: ${{ secrets.VETO_API_KEY }}
run: |
curl -fsS "https://api.veto.so/v1/receipts/export?format=ndjson&limit=10000" \
-H "X-Veto-API-Key: $VETO_API_KEY" \
-o receipts.ndjson
node --input-type=module <<'NODE'
import { readFileSync } from "node:fs";
import {
parseReceiptNdjson,
verifyDecisionReceiptChain,
} from "veto-receipt-protocol";
const receipts = parseReceiptNdjson(readFileSync("receipts.ndjson", "utf8"));
const result = verifyDecisionReceiptChain(receipts);
if (!result.ok) {
throw new Error(`receipt chain broke at ${result.breakAt}: ${result.reason}`);
}
NODEThe Node process exits non-zero when verification fails, so the job turns red on any tampering or gap.
4. Inspect a single decision
To pull one canonical payload — for an incident review or to attach to a ticket:
curl https://api.veto.so/v1/decisions/<decision-id>/receipt \
-H "X-Veto-API-Key: $VETO_API_KEY"5. Forward to a SIEM or archive
Because each line is self-describing canonical JSON, NDJSON drops straight into log pipelines. Verify before you forward so you never ingest a broken chain:
node verify-receipts.js \
&& curl -X POST "$SIEM_INGEST_URL" \
-H "content-type: application/x-ndjson" \
--data-binary @receipts.ndjsonStore the raw NDJSON alongside the final receipt summary so anyone can re-verify the exported chain later.
Related
- Decision receipts — the model and threat properties
- Receipts API — endpoint and schema reference
- SDK reference — protocol helpers and JSON Schema exports

