Obexal Docs

Docs/Administration/Audit log

Audit log

An append-only, tenant-scoped audit log: over a hundred typed actions, a real-time stream, SIEM export and a retention purge that is the only sanctioned way to delete anything.

Everything that matters in Obexal, from a sign-in to a policy change to an AI agent delegation, is written to a single audit log, scoped to your organization and immutable by design. This page covers what is logged, how to read it (console, API, real-time stream), how to get it out, and how retention works.

What gets logged

The log records over a hundred typed actions (110 today). Each action is a stable dotted identifier, so your SIEM rules never depend on display text. The main families:

FamilyExamples
user.*user.signup, user.login.success, user.login.failed, user.session.revoked
auth.*MFA (auth.mfa.success), passkeys (auth.webauthn.register), passwordless, SAML, LDAP and social sign-ins, password resets, auth.account.locked
mfa.*factor lifecycle: mfa.totp.activate, mfa.recovery.regenerate
oauth.*oauth.consent.granted, oauth.token.exchange (agent delegation), oauth.signing_key.rotated
admin.*members, roles and groups, tenant settings, access policies, IP blocks, invitations, custom domains, API tokens, agent governance (admin.agent.policy_updated, admin.agent.secret_rotated)
scim.*inbound provisioning: scim.user.provisioned, scim.user.deactivated
agents and securityagent.auto_contained, security.incident.acked
platformtenant.self_signup, operator.tenant.suspended, webhook.endpoint.created, crypto.secrets.reencrypted

Anatomy of an event

As returned by the API, an event looks like this:

{
  "id": "b7f3c2e8-5f9a-4c1d-9e2b-7a6d54c3f0a1",
  "action": "oauth.token.exchange",
  "target": "agent:agent-support-bot",
  "actorUserId": "8c1e35a2-90d7-4b2e-b6a1-53d92f7c44e0",
  "actorEmail": "alice@example.eu",
  "ip": "203.0.113.10",
  "userAgent": "support-bot/1.4",
  "createdAt": "2026-07-02T09:14:03Z",
  "metadata": {
    "agent": "agent-support-bot",
    "agentName": "Support bot",
    "scope": "tickets:read",
    "audience": "https://api.example.eu",
    "chained": false
  }
}

The ip field is resolved defensively. By default it is the TCP peer address, which cannot be spoofed. X-Forwarded-For is honored only when the connection actually comes from a reverse proxy declared in TRUSTED_PROXIES (self-hosting), and then the rightmost address that is not a trusted proxy is used: a client can never choose the IP that gets audited.

Immutable by design

The log is append-only, and that property is enforced in the database itself, not just in application code: a trigger on the audit table rejects every UPDATE and DELETE. The single sanctioned exception is the retention purger, whose DELETE runs inside a transaction that sets the app.allow_audit_purge flag (SET LOCAL, so it is scoped to that one transaction). No other code path sets that flag.

Query the log

The console shows the log in the Audit section, updated in real time. Over the API, GET /v1/admin/audit requires the audit:view permission, with an Admin API token (Authorization: Bearer obx_...) or a console session. If your organization uses a custom domain, it replaces accounts.obexal.com.

ParameterMeaning
qfull-text search, capped at 200 characters
outcomeok, warn or danger (any other value is ignored)
limitpage size, default 100, maximum 500
offsetpagination offset
curl -sS "https://accounts.obexal.com/v1/admin/audit?q=agent&outcome=danger&limit=50" \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN"
# 200 -> {"events": [...], "total": 3}

Real-time stream

GET /v1/admin/audit/stream is a Server-Sent Events (SSE) stream: each new audit write in your organization pushes an audit event carrying the entry as JSON, and a comment ping every 25 seconds keeps the connection alive. The console uses it to refresh the audit view live; any SSE client works.

curl -N https://accounts.obexal.com/v1/admin/audit/stream \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN"
# : connected
# event: audit
# data: {"action":"user.login.success","createdAt":"2026-07-02T09:14:03Z", ...}

Export to a SIEM

GET /v1/admin/audit/export?format=csv|json (permission audit:view) returns a downloadable attachment with the most recent events, capped at 10000. When the cap is reached, the response carries the header X-Obexal-Truncated: true (and "truncated": true in the JSON body): treat that as a signal to ingest more frequently, not as the full history. The CSV columns are createdAt, action, target, actorUserId, ip, userAgent; the JSON format also includes metadata.

For continuous ingestion, prefer the SSE stream or periodic incremental pulls of GET /v1/admin/audit.

Retention and purge

Retention is a deployment-level setting: AUDIT_RETENTION takes a duration, and 0 (the default) means nothing is ever deleted. When a retention window is set, a purger deletes entries older than the cutoff, once at startup and then every 24 hours, in line with the GDPR storage limitation principle. That purge is the only path allowed through the immutability trigger, via the app.allow_audit_purge transaction flag described above. See Configuration and GDPR.