Skip to content

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}/audit

Role: 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.

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.

There is no HTTP endpoint for chain verification. Run:

Terminal window
z4j audit verify

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