Audit logs at /settings/org/audit-logs record administrative and collaboration actions inside your org. The page is scoped to your org. You cannot see events from other orgs, and a user landing on this page can only see events from the org they belong to.
What is logged
The audit log captures state-changing actions on collaboration and admin surfaces. Read-only operations (listing teams, viewing a member, calling a chat model) are not in the audit table.
Today the logged actions are:
| Action | Resource type | When it fires |
|---|---|---|
create |
team |
A new team is created |
update |
team |
A team's name, description, or settings change |
delete |
team |
A team is deactivated |
add_member |
team |
A user is added to a team |
remove_member |
team |
A user is removed from a team |
update_member_role |
team |
A team member's role changes |
share |
source |
A source is shared with another user or team |
respond_share |
source |
A user accepts or declines a share invitation |
revoke_share |
source |
A share is revoked |
invite |
conversation_participant |
A user is invited into a conversation |
invite_team |
conversation_participant |
A team is invited into a conversation |
respond_invite |
conversation_invite |
A user accepts or declines a conversation invitation |
remove |
conversation_participant |
A participant is removed from a conversation |
activate |
global_source |
A platform-managed source is activated for the org |
deactivate |
global_source |
A platform-managed source is deactivated |
update_config |
global_source |
A platform-managed source's configuration changes |
Each entry stores:
- Timestamp (UTC)
- User (email and display name, resolved at query time from the user table)
- Action and resource type (from the table above)
- Resource ID (the affected team, source, conversation, etc.)
- Detail: a small JSON blob, typically
{ method, path }for the originating HTTP request - IP address of the request
The Compliance API exposes the same records over a programmatic endpoint (when enabled for your org). See Compliance API below for endpoint details, or /settings/org/compliance-api in the app to mint a key.
Layout
The page has four blocks stacked top to bottom:
- Summary cards: total events, unique users, unique actions over the selected range
- Top Actions: pill-shaped badges sorted by count, clickable to filter the table below
- Filters and export: date range, action filter, page size, export menu
- Log entries table: paginated, sorted newest first
Click any row to expand it inline; the row's detail JSON renders as a preformatted block. Rows with empty detail are not expandable.
Filtering
Three filters control what you see:
- Date range. Presets are 7, 30, and 90 days. Selecting "Custom range" reveals two date inputs. The from/to dates use the browser's local timezone for the picker, then expand to a full UTC day on the server; picking the same day twice returns the full 24-hour window.
- Action. Either "All actions" (the default) or one of the actions present in the current summary. Clicking a badge in Top Actions sets this filter; clicking it again clears it.
- Page size. 25, 50, or 100 rows per page.
Changing any filter resets pagination to page 1.
Exporting
The Export dropdown next to the filters offers two formats:
- CSV: one row per event, flattened. The
detailblob is stringified into its own column. - JSON: full fidelity, including the nested detail blob.
The export respects whatever filters are currently set on the page. There is no separate "download everything" path; narrow the range first, then export.
The download streams directly to disk through the browser. It uses your session cookies; you do not need an API key.
Retention
Audit log entries are stored indefinitely in your org's database. There is no automatic purge today. The Compliance API can read any record from the org's full history.
If you need an explicit retention policy enforced (for example, automatic deletion after 365 days for GDPR compliance) that is a data-handling configuration that lives outside the app UI; raise it with your PrivateMind account contact.
Compliance API
The Compliance API exposes audit log records over a programmatic endpoint, designed for SIEM ingestion and automated compliance pipelines. It returns the same records visible in the UI, with cursor-based pagination and no session-cookie dependency.
Enabling the API
An org admin must enable the Compliance API at Settings → Org → Compliance API (/settings/org/compliance-api), then mint a compliance key from that page. The key carries the audit_logs:read scope and is separate from the API keys used for inference.
Disabling the toggle is an immediate kill switch: outstanding keys stop working straight away, they do not need to be revoked first.
Authentication
Compliance keys are minted as an access-key-id and secret joined by a colon. Pass the whole string, exactly as shown when you minted it, as a bearer token:
Authorization: Bearer <ACCESS_KEY_ID>:<SECRET_ACCESS_KEY>Do not split the two halves across separate headers, and do not add or strip a prefix — the value is sent verbatim. A token that is not exactly two colon-separated parts returns 401.
Endpoint
curl -s "https://privatemind.com/api/v1/compliance/audit-logs?from=2026-08-01&page_size=100" \
-H "Authorization: Bearer $COMPLIANCE_KEY"Note the host: the Compliance API is served by the application, not by api.privatemind.com (which serves the inference API).
Query parameters
All parameters are optional. Without filters the endpoint returns the most recent events for your org.
| Param | Type | Meaning |
|---|---|---|
from |
RFC 3339 datetime or YYYY-MM-DD date |
Lower bound, inclusive (created_at >= from). |
to |
RFC 3339 datetime or YYYY-MM-DD date |
Upper bound, exclusive (created_at < to). |
action |
string | Filter to a single action, e.g. user_signed_in. Enumerate valid values from event types rather than guessing. |
page_size |
integer | Page size. Default 100, maximum 1000; larger values are clamped to 1000, not rejected. |
cursor |
opaque string | Continuation token from the previous page's next_cursor. |
There is no resource_type filter, and no limit/offset — pagination is cursor-based (see below).
A from or to value that is not a parsable date is ignored silently rather than rejected, so the response comes back unfiltered on that bound. Validate your date formatting before trusting a result set.
Filtering by action matches across the legacy action rename map in both directions — a filter on login also returns rows stored as user_signed_in, and vice versa. Stored rows are never rewritten, so this is what keeps a filter stable across the taxonomy cutover.
Response
{
"data": [
{
"id": "18472",
"timestamp": "2026-08-15T09:31:12.442Z",
"actor": "jane.smith@example.com",
"api_key_id": null,
"action": "team_created",
"resource_type": "team",
"resource_id": "team-abc123",
"metadata": { "method": "POST", "path": "/v1/teams" }
}
],
"pagination": { "next_cursor": "eyJ0IjoiMjAyNi0wOC0xNVQwOTozMToxMi40NDJaIiwiaSI6MTg0NzJ9", "has_more": true }
}| Field | Meaning |
|---|---|
id |
Record id, serialised as a string. |
timestamp |
UTC timestamp of the event. |
actor |
For key-authenticated actions, the key's label (falling back to its id). For human actions, the user's email. null for system-driven events — surfaced explicitly rather than masked behind a placeholder. |
api_key_id |
The key that performed the action, or null when a human did. |
action |
The action name. |
resource_type |
The type of resource affected. |
resource_id |
Id of the affected team, source, conversation, and so on. |
metadata |
Small JSON object, { method, path } for the originating HTTP request. {} when there is no detail. |
Records are sorted newest-first (timestamp descending, id descending as a tiebreaker so pages stay stable when several rows land in the same millisecond).
There is no success field on this endpoint's success responses, and no user_email, user_display_name, detail, ip_address, or created_at field. Caller IP is recorded server-side but is not returned.
Pagination
Pagination is keyset (cursor) based, not offset based. To walk the full set, pass the previous response's next_cursor back as cursor, and stop when has_more is false:
CURSOR=""
while : ; do
PAGE=$(curl -s "https://privatemind.com/api/v1/compliance/audit-logs?page_size=1000${CURSOR:+&cursor=$CURSOR}" \
-H "Authorization: Bearer $COMPLIANCE_KEY")
echo "$PAGE" | jq -c '.data[]'
[ "$(echo "$PAGE" | jq -r '.pagination.has_more')" = "true" ] || break
CURSOR=$(echo "$PAGE" | jq -r '.pagination.next_cursor')
donenext_cursor is null on the last page. The cursor is opaque — round-trip it unchanged. Do not decode, construct, or increment it; a malformed or stale cursor returns 400 invalid_cursor.
Event types
curl -s "https://privatemind.com/api/v1/compliance/audit-logs/event-types" \
-H "Authorization: Bearer $COMPLIANCE_KEY"Returns the declared event surface, so filters and dashboards can be built against the registry instead of scraping stored history:
{
"data": [
{
"action": "team_created",
"category": "org",
"resource_type": "team",
"description": "A team was created.",
"legacy_actions": ["create"]
}
]
}legacy_actions lists the older action strings an event replaced. Stored history keeps those values, which is why filtering on either convention matches both.
Rate limits
Requests are limited per compliance key, at the key's configured rate (60 per minute by default). Every response carries:
x-ratelimit-limit-requestsx-ratelimit-remaining-requests
A throttled request returns 429 and additionally carries Retry-After (seconds) and x-ratelimit-reset-requests (an ISO timestamp).
Reads are themselves audited
Every successful call writes its own audit record, with action compliance_audit_logs_read and resource type compliance_api_key, attributed to the calling key. Expect to see your own polling in the feed — that is deliberate, so that "who read the audit log" is itself auditable.
Error responses
| Status | error.type |
Meaning |
|---|---|---|
400 |
invalid_cursor |
The cursor is malformed or no longer decodable. Restart the walk without a cursor. |
401 |
— | Missing, malformed, expired, or invalid key. The body carries a message rather than an error object. |
403 |
insufficient_scope |
The key does not carry audit_logs:read. Also returned when no bearer token is supplied at all. |
403 |
feature_disabled |
The Compliance API is not enabled for the org, or the key resolves to no org context. |
429 |
rate_limit_error |
Rate limit exceeded. Honour Retry-After. |
500 |
— | The query failed. Safe to retry. |
503 |
— | Key authentication is temporarily unavailable. Retry with backoff. |
See Errors for the general error-envelope contract; note that this endpoint's success responses deliberately depart from it.
What is not logged
- Chat messages and model calls. The content of user prompts and model responses is not in the audit log. Conversation history lives in your org's conversation tables and has its own retention model.
- Reads. Listing teams, viewing a user's profile, opening a conversation: none of these produce audit entries.
- Failed requests. Audit entries are only written for requests with status
< 400. A403attempt to delete someone else's team is not recorded in this table. - Sign-in events. Authentication is handled by the identity provider; check the IdP's own audit log for sign-in trails.
Where next
- Org settings: the feature toggles whose flips you'd often want to cross-reference here
- Users and teams: what the
add_member/update_member_roleentries refer to