Audit API
The dedicated audit endpoint is project-scoped and admin-only. A separate
GET /api/v1/activity feed exposes selected audit records to ordinary project
members; it omits user_agent, and only a global admin receives source_ip.
A single audit endpoint serves both pagination and export; there is no separate
/export route. Chain verification is a CLI command (z4j audit verify), not
an HTTP endpoint.
GET /api/v1/projects/{slug}/auditRole: admin.
Query params:
| Param | Type | Notes |
|---|---|---|
action_prefix |
string | Prefix match on action, e.g. auth. or task.retry. |
outcome |
string | success, failure, etc. |
user_id |
UUID | Filter by actor user. |
since |
RFC 3339 | Lower bound on occurred_at. |
cursor |
opaque | Pagination cursor from prior page. |
limit |
int | 1..5000. |
format |
string | csv, json, or xlsx. Switches to export mode (see below). |
fields |
string | Comma-separated column projection. Only honoured in export mode. Unknown column names are silently ignored. |
Response (paginated mode):
{ "items": [ { "id": "01H...", "occurred_at": "...", "user_id": "...", "project_id": "...", "action": "task.retry", "target_type": "task", "target_id": "01H...", "result": "ok", "outcome": "success", "event_id": null, "source_ip": "203.0.113.10", "user_agent": "Mozilla/5.0 ...", "metadata": {"original_task_id": "01H...", "new_task_id": "01H..."} } ], "next_cursor": "..."}row_hmac and prev_row_hmac are not part of this payload. They are chain machinery, not audit content, and the API does not hand them out over HTTP.
Export
Section titled “Export”Set format=csv (or json / xlsx) on the same endpoint to switch to export
mode. Pagination is ignored. CSV and JSON fail loudly if the filter exceeds
50,000 rows; XLSX has a lower 25,000-row in-memory cap and also fails loudly.
Narrow by action_prefix, outcome, or since and re-run.
GET /api/v1/projects/{slug}/audit?format=csv&since=2026-01-01T00:00:00Z&action_prefix=auth.Exports carry the audit columns (id, occurred_at, action, target_type, target_id, result, outcome, user_id, event_id, source_ip, user_agent, metadata), and fields selects a subset of them. They do not carry row_hmac or prev_row_hmac, so an export is a record to ship to a SIEM or hand to an auditor, not something a downstream system can re-verify the chain from. Verification runs against the database, below.
Verifying the chain
Section titled “Verifying the chain”There is no HTTP endpoint for chain verification. Run:
z4j audit verifyThe CLI walks the log and reports row counts for the active and frozen
generations. It counts every finding, but caps rendered detail at the first 100
plus an overflow marker; use the MISMATCHES (N) header for the total. It exits
0 for a clean chain, 1 for an integrity finding or for a verifier/database
failure after settings loaded, and 2 only for settings-load failure or an
invalid --limit. A cron job can page on non-zero, but exit 1 alone does not
distinguish tampering from a run that could not complete.
Pass --known-head with an envelope you exported earlier to also assess whether the current chain still contains that head. The result is one of CURRENT_MATCH, PRUNE_MATCH, CURRENT_PRUNE_MATCH, VERIFIED_ANCESTOR, INVALID, or UNPROVABLE, and it is reported on its own line. A log rolled back past your exported head reports UNPROVABLE, which is a finding even when every retained row verifies.
For a scheduled check, either wrap the CLI in cron or a Kubernetes CronJob, or enable the built-in verifier worker with Z4J_AUDIT_CHAIN_VERIFY_ENABLED=true (off by default, daily by default, leader-gated). See monitoring for the metrics it emits and HMAC audit chain for the chain construction and its limits.