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:
| Family | Examples |
|---|---|
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 security | agent.auto_contained, security.incident.acked |
| platform | tenant.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.
| Parameter | Meaning |
|---|---|
q | full-text search, capped at 200 characters |
outcome | ok, warn or danger (any other value is ignored) |
limit | page size, default 100, maximum 500 |
offset | pagination 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.