Skip to content
/docs

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.ndjson

Paginate 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
done

2. 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}`);
    }
    NODE

The 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.ndjson

Store the raw NDJSON alongside the final receipt summary so anyone can re-verify the exported chain later.