Audit Log
The audit log is a record of the significant things done in your account, by your team and by your end users: who did it, to what, when, and from which address. Nothing can edit it and nothing can delete an entry early — not you, not an API key, not us through the product. Entries are removed only when they pass the retention window of your plan.
You can read it in the dashboard, under Audit log, or through the API (below). Both show the same entries.
There is one log per account. Entries that belong to a project are tagged with it; entries that belong to the whole account (your social login providers, your security settings, your webhook endpoints, the log's own retention) carry no project.
In the dashboard
Open Audit log in the dashboard's menu. The tab shows the log of the project you have selected — its own entries and the account-wide ones — newest first, with the time in your locale (hover for UTC), a readable label for each action, who did it, to what, from which address and device, and a Details control that opens the few facts the entry holds. You can filter by period (the last 24 hours, 7 or 30 days, or a range you choose), by actor, by action and by type of actor, and load more entries as you go. A team member is shown by the email address of their account, matched from the project's member list; an end user or an API key is shown by a short form of its ID.
- Who sees it: the project's owners and admins. A plain member of the project sees a message saying so.
- Retention: the tab shows the current window and your plan's maximum. The account owner can shorten it there, or return to the plan's window. Shortening asks for confirmation first, because entries older than the new window are deleted at the next hourly sweep and cannot be recovered.
What is recorded
| Action | Done by | What happened |
|---|---|---|
key.created | team member | An API key was created. The entry has the key's name and scopes, never the key. |
key.revoked | team member | An API key was revoked. Revoking a key that is already revoked is not an action and is not recorded again. |
project.created | team member | A project was created. |
project.member_added | team member | Someone was added to a project, with their role. |
project.member_removed | team member | Someone was removed from a project, or left it. |
project.ownership_transferred | team member | A project was handed to another member. |
gate.oidc_providers_updated | team member or API key | Social login providers were switched on or off. |
gate.security_updated | team member or API key | The passwordless and MFA switches were changed. |
gate.return_urls_updated | team member or API key | A project's social-login return addresses were replaced. |
gate.webhook_created, gate.webhook_updated, gate.webhook_deleted | team member or API key | A Gate webhook endpoint was registered, changed or removed. The entry says whether the address changed, never what it is. |
passepartout.settings_updated | team member | The Telegram bot settings were changed. The entry says whether the token was replaced, never the token. |
lifecycle.rule_set, lifecycle.rule_removed | team member or API key | An automatic-deletion rule was switched on, changed or removed. |
audit_log.retention_changed | team member | The retention window of this log was changed. |
organization.role_created, organization.role_updated, organization.role_deleted | API key | A role of an organization was defined, changed or deleted. The entry has the number of permissions, not the list. |
organization.member_added, organization.member_role_changed, organization.member_removed | API key, or Nautilus | An end user was given a role, had it changed, or lost it. When a role's deadline passes and your rule removes the membership, Nautilus is the actor (system). |
organization.deleted | API key | An organization was deleted, with its roles and members. |
identity.login | end user | An end user signed in. |
identity.password_changed | end user | An end user changed their password. |
Creating or renaming an organization is not recorded: it grants no access, and a backend that creates one per customer would bury the entries that matter. Creating, changing and deleting cron jobs and delayed jobs, Relay endpoints and end-user identities is not recorded either. New actions may be added over time; an action that exists is never renamed.
What an entry says about a person
An entry names a person by ID, never by email address, name or anything else that identifies them outside Nautilus. What our staff do on your account from the admin panel (for example when you ask support to change something) is recorded the same way, with the actor type staff. An end user's ID is the id Gate gave them; a team member's is their Nautilus account ID; an API key is named by its key ID. To show a name through the API, look the ID up on your side — for a team member, the project's member list in the dashboard shows the address behind each ID; the dashboard's Audit log tab does that matching for you.
Each entry also records the IP address and the user agent of the request. The address is the one that reached us — the client of Nautilus, as our reverse proxy saw it; for a sign-in it is the address the identity service recorded on the session it created. Two things follow:
- When your backend drives a sign-in or a password change for its user through
/v1/gate/auth/*, the address is your backend's, not your user's. When a person signs in in their own browser against Nautilus directly, it is theirs. - For a password change the address is the one the identity service recorded on the person's session, that is, where that session was opened from; it is not read from the request that changed the password. The user agent is that of the request the identity service received: when your backend calls
/v1/gate/auth/settings/submit, your backend's own. - An address that is not a valid IP, or a user agent past 256 characters, is dropped or cut; it is never stored as sent.
When an entry is written
Actions in the dashboard and through the API are written in the same database transaction as the action. Either both happen or neither does: an API key cannot be created without its entry, a refused or failed action leaves no entry, and if the entry cannot be written the action fails with 500 and does not happen.
Two exceptions, stated plainly:
- Gate webhook endpoints are held by our delivery service, which a database transaction cannot include. Nautilus changes the endpoint first and writes the entry right after, so there is never an entry for a change that did not happen. In the rare case that the change was made and the entry could not be written, the call answers
500although the change stands. - A sign-in and a password change happen inside Kratos, the identity service, not in our database. They are written when Kratos tells us, each from its own Kratos hook: a sign-in when Kratos runs its after-login hook, a password change when Kratos runs its
settings.after.passwordhook, that is, when the change has been saved. The entry is then written in the same transaction as the record of the webhook event we already keep for that event, so the log and your webhook always agree. Consequences:- An entry appears a moment after the sign-in or the change, not within it.
- A sign-in Kratos does not report is not recorded. A failed sign-in is not reported by Kratos, so failed attempts are not in the log. A password change Kratos refuses is not a change and is not recorded either.
- Every way of changing a password is recorded, once: through
/v1/gate/auth/settings/submit, by posting to theui.actionof a settings flow, and in Kratos' own browser settings flow. Before 2026-10 only the first was; a change made in a flow that never went through/v1/gate/auth/settings/submithas no entry. Because the entry now comes from Kratos' report and not from our gateway's answer, the entry and theidentity.password_changedwebhook for a change made through that route appear at a slightly different moment than they used to: when Kratos reports the change, not when our gateway has Kratos' answer. - The project of a password change is the project of the key that started the settings flow. A flow no key started (Kratos' own browser flow) belongs to no project; its entry is shown, like the entries of account-level actions, under every project of the account.
- The report is made once and not retried. If our gateway is down at that moment the change stands and has no entry and no event.
- Only your end users' password changes are recorded; a dashboard account's password is not.
- A sign-in through Telegram (Passepartout) is not recorded.
Retention
Entries are kept for the window of your plan and then deleted, hourly:
| Plan | Window |
|---|---|
| Free | 7 days |
| Pro | 30 days |
| Advanced | 90 days |
| Contract | as agreed in the contract; 90 days if it says nothing |
The account owner can shorten the window, from one day up to the plan's window, and cannot lengthen it. If you move to a smaller plan, the plan's window applies from then on, whatever you chose before. An entry past the window is no longer returned, even before the hourly deletion has removed it, and entries removed this way do not come back. Shortening the window removes older entries at the next hourly deletion.
Read the log
GET https://api.vernesoft.com/v1/gate/audit-log
Authorization: Bearer vrn_...
Requires an API key with the gate scope. The key reads its own project's entries and the account-wide ones — never another project's, never another account's. The log is read-only: no route changes or deletes an entry.
Any key with the gate scope can read the whole log, including the IP addresses and user agents of your team members — the people who use your dashboard — and not only those of your end users. There is no separate scope for the log, so treat a gate key as having that access, and keep it on your backend. If you do not want a service to see your team's addresses, do not give it a gate key.
| Query parameter | Meaning |
|---|---|
limit | Entries per page, 1 to 100. Default 20. |
cursor | The next_cursor of the previous page. |
action | One action from the table above, e.g. key.created. |
actor_id | Only entries whose actor has this ID — an end user, a team member or an API key. |
actor_type | team_member, api_key, end_user, system or staff. |
target_id | Only entries whose target has this ID — the end user whose role changed, the key that was revoked. |
from | Only entries at or after this instant (RFC 3339, e.g. 2026-10-13T09:30:00Z). |
to | Only entries before this instant. Must be later than from. |
Filters combine: ?action=identity.login&actor_id=...&from=... is the sign-ins of one person since a date.
curl 'https://api.vernesoft.com/v1/gate/audit-log?action=key.created&limit=2' \
-H 'Authorization: Bearer vrn_...'
Entries come newest first. While has_more is true, pass next_cursor as cursor to get the next page; keep the same filters. The cursor is opaque: pass it back, do not parse it. Entries written while you page do not appear in the pages after the one you are on.
The entry
| Field | Type | Meaning |
|---|---|---|
id | UUID | Unique per entry. |
occurred_at | RFC 3339, UTC, microseconds | When the entry was written. |
action | string | One of the actions above. |
actor.type | string | team_member (your team in the dashboard), api_key, end_user, system (Nautilus, on a rule you set), staff (a member of Nautilus staff acting on your account from the admin panel, named by their own ID), or unknown for a request that arrived without attribution (not expected). |
actor.id | UUID or null | Who. null when it cannot be known. |
target | object or null | What the action was done to: { "type", "id" }. Types include api_key, project, identity, role, organization, webhook_endpoint, lifecycle_rule and setting. |
project_id | UUID or null | The project, or null for an account-wide action. |
details | object | A few facts about the action — scopes, a role change's old and new role, which fields a webhook update touched. Never an email address, a key, a token or a webhook address. |
ip | string or null | The request's IP address. |
user_agent | string or null | The request's user agent, up to 256 characters. |
Fields are added over time, never renamed or removed; read the ones you need and ignore the rest.
Errors
Errors use the standard envelope:
{ "error": { "code": "invalid_payload", "message": "Query parameter `from` must be an RFC 3339 timestamp, e.g. 2026-10-13T09:30:00Z.", "request_id": "req_..." } }
| Status | Code | When |
|---|---|---|
400 | invalid_payload | A parameter is malformed: an unknown action, actor_id that is not a UUID, a bad timestamp, from not before to, a bad cursor. |
401 | unauthorized | No key, or an invalid or revoked one. |
403 | forbidden | The key does not have the gate scope. |