Decision ledger
Every Provenance Authorize authorize and verify call writes an append-only record: which token, which facts, which policy version, which rule, and the result. Months later you can answer "prove a human authorized this" — and prove the record wasn't edited after the fact.
The ledger is per account and tamper-evident: each record carries record_hash = sha256(canonicalize(record)) and a prev_hash linking to the account's previous record — the same hash-chain construction as our video chunk chain. Break any record and the chain no longer recomputes.
The record
| Field | Meaning |
|---|---|
| decision_id | ULID, primary key |
| kind | authorize or verify |
| result | valid | denied | pending |
| reason | the deny/step-up reason code, when not valid |
| facts | the policy fact object as evaluated (amount, action type, biometric_bound, tier…) |
| matched_rule_id, policy_version | which rule of which policy version decided it |
| jti, agent_session_id, device_id | the token/session/device bound |
| record_hash, prev_hash | the per-account hash chain |
| created_at | server timestamp |
Querying
# Newest-first, filterable + cursor-paginated (session or account key).
curl -s "$API/v1/decisions?result=denied&reason=policy_hardware_required" \
-H "Authorization: Bearer $ACCT"
# → { data: [ … ], next_cursor: "01J…" }
curl -s "$API/v1/decisions/$DECISION_ID" -H "Authorization: Bearer $ACCT"
Filters: from/to (ISO time), result, reason, jti, session; page with limit + cursor. In the SDK: prov.decisions.list({ result: "denied" }), prov.decisions.get(id). The dashboard's Decisions view is a filterable table with a row drill-down (facts vs. matched rule vs. policy version).
Verifying integrity
curl -s "$API/v1/decisions/verify-chain" -H "Authorization: Bearer $ACCT"
# → { valid: true, count: 1234 } (or { valid:false, broken_at, reason })
verify-chain recomputes the hash chain over an optional from/to range and reports the first break. In the SDK: prov.decisions.verifyChain(). The dashboard exposes a Verify chain button.
Notes
- Append-only. There is no edit or delete endpoint; account deletion anonymizes actor fields but keeps the chain intact.
- Scope. The ledger is an Authorize account surface — decisions made with a carrier/CLI key (no account) aren't chained here; those are covered by usage/billing records.
- Retention. Durable retention needs Postgres. The in-memory store keeps a bounded ring (
DECISION_LEDGER_MAX, default 10k) and logs a warning when it trims. - Anchoring the chain head. Checkpoints covering each account's chain head are published to the transparency log, so anyone can check that history was never rewritten.