Skip to main content

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

FieldTypeNullableDescription
_idstringnoMongoDB ObjectId
organizationIdstringnoOwning org; chains never mix across tenants
userIdstringyesUser actor; null for system actors (cron, SCIM, chain anchor)
userEmailstringyesUser email at the time of the event
userRolestringnoRole at the time of the event; defaults to member
actionenumnoThe event name (see below)
resourcestringyesThe resource type affected (trace, policy, workspace, …)
resourceIdstringyesThe _id or business identifier of the affected document
statusenumnosuccess or failure
metadataobjectyesFree-form context the action chose to record
ipAddressstringyesOrigin IP from the request
userAgentstringyesOrigin user-agent from the request
chainHashstringyesThe chain link, when chain mode is enabled
previousChainHashstringyesPrevious entry's chainHash, or GENESIS_HASH ("0") for the first entry
createdAtstring (ISO 8601)noAppend timestamp; immutable

The action enum is the canonical taxonomy of what an org-admin review can ask for. Examples:

NamespaceExamples
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 parameterDefaultDescription
page11-indexed page number
limit50Page size; capped server-side
actionFilter by exact action enum
userIdFilter by acting user's id
resourceFilter by resource type (e.g., policy)
resourceIdFilter by exact business id
workspaceIdFilter to one workspace
statussuccess or failure
dateFrom / dateToISO date range on createdAt
searchCase-insensitive substring on detail and resourceId (max 100 chars)
curl
curl "https://api.adjudon.com/api/v1/audit?action=policy.update&page=1&limit=50" \
-H "Authorization: Bearer $ADJUDON_API_KEY"
Python
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"]
Node
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
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 under metadata.actor, not userId.
  • Action enum is open. New action names ship without a major version bump; treat the enum as forward-compatible.
  • Search is constrained. The search filter is a 100-character substring match on detail and resourceId; deeper queries belong on the JSON export.

See also