Audit log
Read and export the audit trail of administrative actions in your organization.
The organization's audit log: who did what, when, from where, on what — and, for actions taken through this API, with which token. It is the same log the portal shows, and it is append-only: no endpoint edits or deletes an entry.
| Endpoint | Scope | Token owner's role |
|---|---|---|
GET /audit | audit:read | Admin |
GET /audit/export | audit:read | Admin |
Both routes need an owner who is currently an organization Admin; a
Member's token gets 403 insufficient_scope. An organization sees its own
log and nothing else.
What is recorded
| Recorded | Examples |
|---|---|
| Writes on this API | project_create, project_rename, project_archived, key_create, key_rotate, member_role_update, invite_revoke, org_settings_update, … — the same action names the portal records. The table below maps each call to its action. |
| Token lifecycle | pat_create, pat_revoke (with the token id, name, scopes and expiry). |
| Secret replays | key_create_replayed, key_rotate_replayed — an idempotent replay delivered a secret a second time. |
| Bulk exports | audit_export, usage_export, with row_count and format. |
| Portal actions | The same entries the portal's activity log shows, without the token fields. |
Which action each call records
The action names are the portal's, so some differ from the API verbs:
| API call | action |
|---|---|
PATCH /org | org_settings_update |
PATCH /members/{id} — role | member_role_update |
PATCH /members/{id} — name | member_update |
POST /members/{id}/disable, POST /members/{id}/enable | member_update (metadata.is_active is false or true) |
PUT /members/{id}/projects | member_projects_update |
DELETE /members/{id} | member_remove |
DELETE /members/{id}/sessions | member_sessions_revoke |
POST /invites | invite_create |
DELETE /invites/{id} | invite_revoke |
POST /projects | project_create |
PATCH /projects/{id} — name or description | project_rename |
POST /projects/{id}/disable | project_archived |
POST /projects/{id}/enable | project_unarchive |
DELETE /projects/{id} | project_purged |
POST /projects/{id}/keys | key_create |
PATCH /keys/{id} — name | key_rename |
PATCH /keys/{id} — ip_allowlist | key_ip_allowlist_update |
POST /keys/{id}/disable | key_deactivate |
POST /keys/{id}/enable | key_activate |
POST /keys/{id}/rotate | key_rotate |
POST /keys/{id}/rotate/cancel | key_rotate_cancel |
DELETE /keys/{id} | key_revoke |
A project's guardrails, and a key's allowed_models, rpm_limit,
expires_at and description, are not yet recorded as separate entries
when they change.
Ordinary reads are not individually audited — an Admin reading their own organization's data is not a privacy event. Exports are the exception, because an export is what a leak looks like when it is one.
The audit entry
| Field | Type | Notes |
|---|---|---|
id | string | Opaque id. |
created_at | string | When, ISO-8601 UTC. |
actor | object | Who: { "user_id", "email" }. For API calls, the token's owner. |
action | string | What, e.g. key_rotate. |
resource_type, resource_id | string | null | On what, e.g. api_key and its id. |
metadata | object | Action-specific detail (for example row_count on an export). |
ip_address | string | null | From where. |
Which token acted. An entry written by a call made with a personal
access token carries the token in metadata, next to the action's own
detail:
metadata key | Value |
|---|---|
credential | "pat" |
pat_id | The token's id. |
pat_name | The token's name. |
scope | The scope the call used, e.g. keys:write. |
Portal actions have none of these keys.
{
"id": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
"created_at": "2026-09-24T10:05:31Z",
"actor": { "user_id": "8e7d6c5b-4a39-4281-9f0e-1d2c3b4a5968", "email": "[email protected]" },
"action": "key_rotate",
"resource_type": "api_key",
"resource_id": "c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74",
"metadata": {
"grace_expires_at": "2026-09-24T12:05:31Z",
"credential": "pat",
"pat_id": "d9e8f7a6-b5c4-4d3e-9f2a-1b0c9d8e7f6a",
"pat_name": "CI · payments-deploy",
"scope": "keys:write"
},
"ip_address": "203.0.113.24"
}List audit entries
GET /api/v1/mgmt/auditNewest first, cursor-paginated.
| Query parameter | Type | Notes |
|---|---|---|
actor | string | The actor's user id, or their email address (case-insensitive). |
action | string | Exact action name, e.g. key_revoke. |
from | string | RFC 3339, inclusive. |
to | string | RFC 3339, exclusive. Must be after from. |
limit, cursor | Pagination. |
Any other parameter returns 422 unsupported_filter; a malformed actor,
from or to is 400 invalid_request with param naming it.
curl -G https://portal.<domain>/api/v1/mgmt/audit \
-H "Authorization: Bearer $TF_PAT" \
-d [email protected] \
-d from=2026-09-23T00:00:00Z \
-d limit=200Export the audit log
GET /api/v1/mgmt/audit/exportStreams every matching entry, newest first, in one response — for SIEM ingestion or an evidence request.
| Query parameter | Type | Required | Notes |
|---|---|---|---|
format | string | no | jsonl (default; one audit entry per line, application/x-ndjson) or csv. |
actor, action, from, to | no | As for GET /audit. |
The CSV columns are id, created_at, actor_user_id, actor_email,
action, resource_type, resource_id, ip_address and metadata (the
metadata object as JSON).
At most 10,000 entries per call. A filter that matches more is refused
with 422 export_too_large before anything streams — narrow from/to and
export in windows, or page through GET /audit. Every completed export is
itself recorded as audit_export with the row_count.
curl -G https://portal.<domain>/api/v1/mgmt/audit/export \
-H "Authorization: Bearer $TF_PAT" \
-d format=jsonl \
-d from=2026-09-01T00:00:00Z \
-d to=2026-10-01T00:00:00Z \
-o audit-2026-09.jsonlTip
For an access review, export a period and group by metadata.pat_id:
you get every token that acted, whose it is, which scopes it used, and when.
Errors specific to the audit log
| Status | Code | When |
|---|---|---|
| 400 | invalid_request (param set) | actor neither a user id nor an email; from/to not RFC 3339; from not before to; format not jsonl/csv. |
| 403 | insufficient_scope | Token lacks audit:read, or the owner is not currently an Admin. |
| 422 | unsupported_filter | An unknown query parameter. |
| 422 | export_too_large | The export matches more than 10,000 entries. |