Operations Audit Log
The AuditLog resource is the per-organisation chain of admin
events — policy edits, key rotations, login events, SCIM
provisioning, trace exports, and every other state-changing action
performed by an authenticated user. It is distinct from the
Decision Hash Chain: that chain
records every AI decision; this chain records every admin event.
Two chains, separate concerns, separate consumers. Tested against
API version v1.
All endpoints require an admin or owner role and the auditLog
feature gate (Scale plan and above; PDF export is Governance and
above).
The AuditLog object
| Field | Type | Nullable | Description |
|---|---|---|---|
_id | string | no | MongoDB ObjectId |
organizationId | string | no | Owning org; chains never mix across tenants |
userId | string | yes | User actor; null for system actors (cron, SCIM, chain anchor) |
userEmail | string | yes | User email at the time of the event |
userRole | string | no | Role at the time of the event; defaults to member |
action | enum | no | The event name (see below) |
resource | string | yes | The resource type affected (trace, policy, workspace, …) |
resourceId | string | yes | The _id or business identifier of the affected document |
status | enum | no | success or failure |
metadata | object | yes | Free-form context the action chose to record |
ipAddress | string | yes | Origin IP from the request |
userAgent | string | yes | Origin user-agent from the request |
chainHash | string | yes | The chain link, when chain mode is enabled |
previousChainHash | string | yes | Previous entry's chainHash, or GENESIS_HASH ("0") for the first entry |
createdAt | string (ISO 8601) | no | Append timestamp; immutable |
The action enum is the canonical taxonomy of what an org-admin
review can ask for. Examples:
| Namespace | Examples |
|---|---|
auth.* | auth.login, auth.logout, auth.passkey_registered, auth.sso.initiate, auth.sso.failure |
scim.* | scim.user.create, scim.user.deactivate, scim.group.member_add |
trace.* | trace.export, trace.bulk_delete, trace.bulk_status_update |
policy.* | policy.create, policy.update, policy.delete |
agent.* | agent.create, agent.regenerate_key |
webhook.* | webhook.create, webhook.update, webhook.test |
review.* | review.decision, review.bulk_decision, review.escalate |
incident.* | incident.create, incident.transition, incident.checkpoint_complete |
fria.* | fria.create, fria.transition, fria.delete |
team.* | team.invite, team.remove_member, team.update_role |
The full list is in backend/models/AuditLog.js; new actions are
added without breaking changes — consumers should treat
unknown actions as opaque strings rather than failing closed.
Endpoints
GET /api/v1/audit
GET /api/v1/audit/verify
GET /api/v1/audit/export
GET /api/v1/audit/export/pdf
All four are read-only. The chain is append-only by construction (Cardinal Rule 5: no audit-log entry is ever updated or hard-deleted).
List audit entries
GET /api/v1/audit
| Query parameter | Default | Description |
|---|---|---|
page | 1 | 1-indexed page number |
limit | 50 | Page size; capped server-side |
action | — | Filter by exact action enum |
userId | — | Filter by acting user's id |
resource | — | Filter by resource type (e.g., policy) |
resourceId | — | Filter by exact business id |
workspaceId | — | Filter to one workspace |
status | — | success or failure |
dateFrom / dateTo | — | ISO date range on createdAt |
search | — | Case-insensitive substring on detail and resourceId (max 100 chars) |
curl "https://api.adjudon.com/api/v1/audit?action=policy.update&page=1&limit=50" \
-H "Authorization: Bearer $ADJUDON_API_KEY"
import requests, os
r = requests.get(
"https://api.adjudon.com/api/v1/audit",
params={"action": "policy.update", "page": 1, "limit": 50},
headers={"Authorization": f"Bearer {os.environ['ADJUDON_API_KEY']}"},
)
data = r.json()["data"]
const url = new URL("https://api.adjudon.com/api/v1/audit");
url.searchParams.set("action", "policy.update");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.ADJUDON_API_KEY}` },
});
const { data } = await res.json();
Response (200 OK):
{
"success": true,
"data": {
"entries": [/* AuditLog[] */],
"pagination": { "page": 1, "limit": 50, "total": 1247, "pages": 25 }
}
}
Errors: 401, 403 (FORBIDDEN for non-admin / UPGRADE_REQUIRED
for plan gate), 500 INTERNAL_ERROR.
Verify the chain
Re-replays the operations chain for the calling org. For each entry
ordered by createdAt, recomputes the chain hash from the previous
entry's chainHash and the canonicalised current entry, and compares
to the stored value.
GET /api/v1/audit/verify
curl https://api.adjudon.com/api/v1/audit/verify \
-H "Authorization: Bearer $ADJUDON_API_KEY"
Response (200 OK, success):
{
"success": true,
"data": {
"verified": true,
"totalEntries": 1247,
"lastHash": "9e2d...4c7a"
}
}
Response (200 OK, broken):
{
"success": true,
"data": {
"verified": false,
"totalEntries": 1247,
"lastHash": "5f1c...8b2e",
"error": "Chain break at entry 412: previousChainHash mismatch"
}
}
The verify path detects two failure modes: a previousChainHash
mismatch (the chain link is broken) or a chainHash mismatch (an
entry was altered after the fact). Errors: 401, 403, 500
INTERNAL_ERROR.
Export audit logs (JSON)
GET /api/v1/audit/export
Accepts the same filter query parameters as the list endpoint; returns
the matching entries as a streaming JSON document suitable for
archival. Errors: 401, 403, 500 INTERNAL_ERROR.
Export audit logs (PDF)
GET /api/v1/audit/export/pdf
A formatted PDF rendering of the same entries; intended for compliance
reviewers who prefer a fixed-layout artefact. Plan-gated by the
auditLogPdf feature (Governance and above). Errors: 401, 403
UPGRADE_REQUIRED, 500.
Common gotchas
- Two chains, two endpoints. The Decision Hash Chain
(
/api/v1/hash-chain/*) records every AI decision the agent produces; the Operations Audit Log (/api/v1/audit/*) records every admin event a human or system actor performs. Both are SHA-256 chains, both are append-only, but neither replays the other — choose the right endpoint for the question you are asking. - System actors carry
userId: null. SCIM provisioning, cron retention, and chain-anchor automation log undermetadata.actor, notuserId. - Action enum is open. New action names ship without a major version bump; treat the enum as forward-compatible.
- Search is constrained. The
searchfilter is a 100-character substring match ondetailandresourceId; deeper queries belong on the JSON export.
See also
- Audit Log & Security — the concept page covering both chains and the four-step verify algorithm
- Hash Chain API — the Decision Hash Chain (per-trace) endpoint surface
- Error Codes — the full error taxonomy
- Authentication — admin/owner role requirements