Tamper-evident audit log of every policy decision, write, and forget.
Audit
Every policy decision, write, and forget produces an audit row. The audit log is append-only and SHA-256-chained — POST /v1/audit/verify lets external auditors confirm a row hasn't been tampered with.
GET /v1/audit
GET /v1/audit
?actor=user:alice
&capability=forget.gdpr
&decision=allow
&limit=100
| Query param | Notes |
|---|---|
| actor | Filter by actor ID |
| capability | Filter by capability name |
| decision | allow or deny |
| endpoint | Filter by endpoint path |
| since / until | RFC 3339 time bounds |
Response
{
"items": [
{
"id": "audit_01HX...",
"ts": "2026-05-15T10:42:09Z",
"actor": "user:alice",
"token_jti": "...",
"tenant_id": "acme",
"scope": "org:acme/dept:eng/user:alice",
"endpoint": "/v1/experience",
"capability": "scope.write",
"decision": "allow",
"decided_by_tier": "scope",
"request": { "body_sha256": "..." },
"response_status": 202,
"event_id": "evt_01HX...",
"client_ip": "203.0.113.42",
"request_id": "req_01HX...",
"elapsed_ms": 4,
"gdpr": false
}
]
}
GET /v1/audit/
Single audit row.
POST /v1/audit/verify
Confirm an audit row hasn't been tampered with. Pass the row ID and the exact body you have on file; the server recomputes the hash and reports a match.
{
"audit_id": "audit_01HX...",
"body": "<canonicalized JSON of the row>"
}
Response
{
"match": true,
"algorithm": "sha256"
}
match: false doesn't mean tampering — it can also mean canonicalization differs (use the body you got from GET /v1/audit/{id} verbatim).