Reading the audit log
The sixty-four identity events, the four fields the screen does not show you, why a failed sign-in can be invisible, and the tamper-evidence claim that is not true of this trail.
The identity audit log records what people did to accounts, roles, sessions and sign-on configuration in your organisation. It is the record you read when somebody asks who changed that, and the one you show an auditor who asks whether administrative activity is recorded at all.
This article covers what is in it, what is not, and one claim the interface makes about it that is not true. Read the last section before you write anything about tamper-evidence into a policy document.
Where it is
Identity → Audit Log in the sidebar; the page itself is headed Audit. Its description: Tenant-scoped audit trail. Filter by actor, action, time window.
The table has six columns — Timestamp, Actor, Action, Target, Outcome, IP — and the outcome is rendered as a plain OK or FAIL pill. Actors are shown as a truncated identifier with the full value in the tooltip. Missing values render as an em dash.
Filtering is five controls: a from and a to time, an actor user id, a dropdown of the actions present in the loaded rows, and a free-text search across actor, action and resource. Export CSV downloads what you are looking at as qorionix-audit-YYYY-MM-DD.csv with the header row timestamp,actor,action,resource,outcome,ip.
Who can read it
There are two read surfaces and they behave differently.
| Endpoint | Returns | Gate |
|---|---|---|
GET /api/v1/audit-logs | Your own actions only. The user is taken from your session and a ?user_id= parameter is ignored — that parameter was once a cross-tenant read, and the platform now logs a note when somebody supplies one. | A session. No permission. |
GET /api/v1/organizations/{orgID}/audit | Everything in the organisation, filterable by actor, action and time window. Default 100 rows, maximum 500, newest first. Now also filterable by event_type, outcome, target_type and target_id, with ?format=csv or ?format=json for export — see the note below. | organizations.read |
Four fields exist and are never shown to you
A row carries more than the API returns. The response gives you the identifier, actor, action, resource type, resource id, IP address, success flag and timestamp. It does not give you:
metadata— a free-form document per event, where the detail of what actually changed lives. This is the field you would most want during an investigation, and no HTTP path returns it.user_agent— recorded and not returned.organization_id— recorded and not returned, which matters for the next point.tenant_scope— whether a row is attributed to a tenant, to nobody, or to the platform.
The practical consequence: for anything beyond who did what and when, the console and the API will not answer, and the detail has to be read from the database. If you are assembling evidence for a specific incident, ask us rather than assuming the export is complete.
What is recorded
Sixty-four distinct action names. They group cleanly, and knowing the prefixes is most of what you need to search effectively.
auth.login auth.logout
auth.login_failed auth.login_locked auth.login_locked_out
auth.login_inactive auth.login_email_unverified
auth.account_locked auth.login_stuffing_throttled
auth.login_policy_denied auth.login_policy_unavailable
auth.login_signon_policy_denied
auth.login_signon_policy_mfa_enrollment_required
auth.login_step_up_required auth.token_refresh
auth.token_reuse_detected auth.organization_switched
auth.organization_switch_denied auth.oidc_login_startedmfa.enroll_started mfa.enabled mfa.disabled
mfa.enroll_verify_failed mfa.disable_failed mfa.admin_disabled
mfa.webauthn_success mfa.recovery_codes_generated
mfa.recovery_codes_denied
auth.mfa_failed auth.mfa_code_replayed auth.mfa_token_replayed
auth.mfa_recovery_used auth.mfa_recovery_failed
auth.mfa_factor_insufficient auth.mfa_enrolled_during_signin
auth.step_up_verified auth.step_up_enrolled auth.step_up_failed
auth.step_up_throttled auth.step_up_code_replayed
auth.step_up_recovery_used auth.step_up_recovery_failed
auth.password_changed auth.password_change_failed
auth.password_reuse_rejected auth.password_reset_by_admin
auth.admin_password_reset auth.admin_password_reset_reuse_rejectedorganization.created organization.renamed
organization.member_added organization.api_key_created
organization.api_key_revoked
organization.bootstrap_admin_ensured
organization.bootstrap_admin_membership_created
user.register user.email user.status_changed
user.sessions_revoked
role.granted
admin.delegation_granted admin.delegation_revoked
group.rule_membership_added group.rule_membership_removedThree of those are worth an alert rule of your own, because each has few innocent explanations: auth.token_reuse_detected is a refresh credential being replayed, which is the signature of a stolen session; auth.step_up_code_replayed is a one-time code being presented twice; and a run of auth.step_up_throttled is somebody guessing.
What is not recorded, and one of them will surprise you
Also unrecorded: every read, including reads of this log; single sign-on provider creation, change and deletion; role creation, editing and deletion beyond the single role.granted event; assigning and removing a permission; email verification; and accepting an invitation.
That is a real gap in an identity audit trail and it is stated here rather than left for an auditor to find. If you need proof of who changed a federation trust, the answer today is that this log will not tell you.
Why a failed sign-in can be invisible
This is the most useful thing in the article for anybody investigating a suspected password-spraying attempt.
A wrong password for an existing user writes auth.login_failed attributed to that user's organisation, and it appears in your organisation's audit log normally.
A sign-in attempt against an address that does not exist has no user and therefore no tenant. It is still recorded, as an unattributed row — but an unattributed row belongs to no organisation, so it does not appear in GET /organizations/{orgID}/audit at all.
The consequence: an attacker guessing addresses at your domain produces nothing you can see, and only their guesses that happen to hit a real account show up. A quiet audit log is not evidence that nobody is trying. If you need visibility of that, it has to come from the platform's own logs rather than from your tenant's trail — ask us.
There is a related historical footnote worth knowing if you are reading rows from before September 2026. For a period, callers passed an empty string where an organisation identifier belonged, Postgres rejected it as a malformed value, and the error was discarded — with the result that the large majority of audit rows written in that window carry no organisation and are invisible to a tenant query. The bug is fixed; the rows it produced are still unattributed.
Tamper-evidence: this has changed, and this article was wrong about it
A row written with no resolvable tenant (a pre-authentication failure, for instance) is still written plainly, outside any chain, with its own sequence marker — that part of the design is unchanged and is why an unattributed row, covered below, still exists as a concept.
For the general mechanics of verifying a chain — what "intact" means, what a break looks like, and how to read the result — see Verifying the audit chain, which now applies to identity as well as to the trails it already covered.
There is also a second copy of privileged identity events written into the log platform as ordinary events, under a source named after the identity audit bridge. That copy is not chained, and it is subject to log retention rather than to this table's retention — which is the next section.
Retention: there is none
No expiry column, no partitioning, no pruning job, and no delete anywhere in the codebase. Identity audit rows are kept until somebody removes them by hand.
That cuts both ways and both are worth writing down before you make a commitment. If your policy requires audit records to be kept for a period, this table meets it by default. If your policy requires them to be destroyed after a period — a real GDPR-shaped requirement, since these rows carry IP addresses and user agents — the platform will not do that for you, and you should not tell an auditor that it will.
Using it well
- Filter by actor first, then by time. The actor filter takes a user id rather than an email address, which you get from the Users list.
- Export before you start reasoning. The dropdown of actions only lists actions present in the rows already loaded, so a filter can hide the very event you are looking for. Export the window, then work in the CSV.
- Read a person's last month when they leave, especially if they held
admin. This is not an accusation; it is how you find the undocumented change that breaks something in March. - Do not infer absence. Given what is not recorded — permission denials, reads, most SSO and role changes, and unattributed sign-in failures — an empty result is weak evidence. Use it to confirm what happened, not to prove what did not.