List audit logs
Query the workspace's audit trail for compliance reviews and security investigations. Filter by date range for a review period, by actor for an access certification, or by outcome=failure to find attempts that were rejected.
Use it for exports too. With ?paginate=cursor you can page months of history into a SIEM or data warehouse without running a COUNT(*) on every page.
Path parameters
slug:requiredstringThe workspace slug. It appears in your Plane URLs. In https://app.plane.so/my-team/projects/, the slug is my-team. The audit trail is per workspace; there is no cross-workspace query.
Query parameters: time range
Filters combine with AND. Check your spelling on order_by and paginate. Neither is validated: an unrecognized order_by value falls back to the resource's default ordering, and anything other than paginate=cursor uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.
created_after:optionalstring (date-time)Return entries recorded from this timestamp onward, for example 2026-01-01T00:00:00Z. Pair it with created_before to bound a review period.
created_before:optionalstring (date-time)Return entries recorded up to this timestamp. Send timestamps in UTC. created_at is returned in UTC, and mixing offsets is the usual reason a window comes back empty.
Query parameters: who and what
actor_id:optionalstringReturn only actions taken by one user. This is the same id as member_id on the member rosters, so for an access certification you can read the roster and then make one call per person.
Any actor_id filter excludes entries with no actor (system and anonymous events). To account for everything in a window, run a second query without the filter.
event_name:optionalstringReturn only one specific event, for example member.role_updated. Use it when you already know which action you're looking for. Use category for a whole class of events.
category:optionalstringReturn only one family of events: auth, member, role, settings, integration, webhook, security, or instance.
Use role and member to see who gained access and when, settings to see what changed in the workspace, and auth with outcome=failure to see who tried to sign in and failed.
outcome:optionalstringReturn only entries with this outcome: success or failure. Failed attempts are recorded too, so this filter is the fastest way to spot probing.
target_type:optionalstringReturn only entries acting on this kind of object, for example workspace_member. Combine it with target_id to build the history of a single record.
target_id:optionalstringReturn only entries acting on this specific object. Use ?target_type=…&target_id=… to get the full history of one object.
ip_address:optionalstringReturn only entries recorded from this client IP. From a suspicious entry, filter on its ip_address to see everything else that came from that address.
search:optionalstringFree-text search across the entry. Use it when you have a name or partial text instead of an id.
Ordering
order_by:optionalstringField to sort by. Prefix with - for descending.
-created_at,created_at: newest first, or oldest firstid,-id
Use -created_at for an investigation, created_at for a replayable export. Two entries written in the same instant are separated by sequence_number in the payload.
Pagination
per_page:optionalintegerPage size. Defaults to 50, maximum 200. Use 200 for exports.
offset:optionalintegerNumber of rows to skip from the start of the result set. Maximum 10000. An audit trail passes that quickly, so use cursor pagination to go deeper than a few pages.
paginate:optionalstringSet to cursor for the keyset envelope (no COUNT query), which returns next_cursor and has_more instead of next and total_count. Use it for a full export: it has no offset limit, and it doesn't skip or repeat rows when new entries are written while you page.
count:optionalbooleanDefaults to true. Set to false to skip the COUNT(*) behind total_count; the field is then omitted from the response. Set it on every page when you paginate with offsets. Cursor pagination never runs a COUNT and ignores this parameter.
Response shaping
fields:optionalstringComma-separated list of fields to return on each row. Fields you don't request are left out of the response instead of coming back as null, so a missing key means you didn't ask for it and null means the value is null. id is always included, whether or not you name it.
Pass all to get every requestable field. An unknown field name returns 400 with the list of valid names and the closest match, so a typo is caught instead of being silently ignored.
Requestable here: actor_display_name, actor_email, actor_id, actor_type, category, created_at, event_id, event_name, id, ip_address, metadata, new_value, old_value, outcome, project_id, reason, sequence_number, source, target_display_name, target_id, target_type, user_agent, workspace_id.
See Sparse fields.
Scopes
workspaces.audit_logs:read
Errors
| Status | Code | Cause |
|---|---|---|
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | The feature this endpoint belongs to isn't enabled on your plan, or is switched off. |
403 | forbidden | Your role or token scope can't read this workspace's audit logs. |
404 | not_found | No such workspace, or it's outside your tenant. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
Audit reads are privileged
workspaces.audit_logs:read exposes actor emails, IP addresses, and user agents for the whole workspace. Give export tokens only this scope, keep them out of user-facing clients, and handle the exported data as sensitive.
curl -X GET \
"https://api.plane.so/api/v2/workspaces/my-team/audit-logs/?category=role&created_after=2026-01-01T00:00:00Z&created_before=2026-02-01T00:00:00Z&order_by=-created_at" \
-H "X-Api-Key: $PLANE_API_KEY"import requests
response = requests.get(
"https://api.plane.so/api/v2/workspaces/my-team/audit-logs/",
headers={"X-Api-Key": "your-api-key"},
params={
"category": "role",
"created_after": "2026-01-01T00:00:00Z",
"created_before": "2026-02-01T00:00:00Z",
"order_by": "-created_at",
},
)
print(response.json())const params = new URLSearchParams({
category: "role",
created_after: "2026-01-01T00:00:00Z",
created_before: "2026-02-01T00:00:00Z",
order_by: "-created_at",
});
const response = await fetch(
`https://api.plane.so/api/v2/workspaces/my-team/audit-logs/?${params}`,
{
headers: {
"X-Api-Key": "your-api-key",
},
},
);
const data = await response.json();{
"data": [
{
"id": "5c7f1a08-2b64-4d39-9e17-0a3b8c6d2f41",
"event_id": "e0b41d97-6c23-4a8f-b512-9d7a0e3c5f68",
"sequence_number": 48213,
"event_name": "member.role_updated",
"category": "role",
"outcome": "success",
"source": "platform",
"actor_type": "user",
"actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"actor_display_name": "Priya Raghavan",
"actor_email": "priya@example.com",
"target_type": "workspace_member",
"target_id": "3e8a5d17-9c40-4b2f-81d6-4a7f2b9e0c53",
"target_display_name": "Devansh Kapoor",
"old_value": { "role": "member" },
"new_value": { "role": "owner" },
"reason": "",
"metadata": { "workspace_slug": "my-team" },
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"workspace_id": "d84c6f21-7a30-4e59-b6c8-1f9d5a2e7043",
"project_id": null,
"created_at": "2026-01-14T09:22:41.478363Z"
},
{
"id": "1f6b3c92-7d40-4a85-9c21-8e5d0a4f2b76",
"event_id": "c3a97e15-4b82-4d06-9f31-7a2e8c0d6b53",
"sequence_number": 48120,
"event_name": "role.permissions_updated",
"category": "role",
"outcome": "failure",
"source": "api",
"actor_type": "api_token",
"actor_id": "7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71",
"actor_display_name": "Deploy bot",
"actor_email": "bots@example.com",
"target_type": "role",
"target_id": "release-manager",
"target_display_name": "Release manager",
"old_value": null,
"new_value": null,
"reason": "insufficient_permissions",
"metadata": { "requested": "projects.states:write" },
"ip_address": "198.51.100.7",
"user_agent": "PlaneDeploy/2.4",
"workspace_id": "d84c6f21-7a30-4e59-b6c8-1f9d5a2e7043",
"project_id": null,
"created_at": "2026-01-13T17:04:58.113402Z"
}
],
"next": 50,
"previous": null,
"total_count": 327,
"pagination": {
"style": "offset"
}
}{
"data": [
{
"id": "5c7f1a08-2b64-4d39-9e17-0a3b8c6d2f41",
"event_id": "e0b41d97-6c23-4a8f-b512-9d7a0e3c5f68",
"sequence_number": 48213,
"event_name": "member.role_updated",
"category": "role",
"outcome": "success",
"source": "platform",
"actor_type": "user",
"actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"actor_display_name": "Priya Raghavan",
"actor_email": "priya@example.com",
"target_type": "workspace_member",
"target_id": "3e8a5d17-9c40-4b2f-81d6-4a7f2b9e0c53",
"target_display_name": "Devansh Kapoor",
"old_value": { "role": "member" },
"new_value": { "role": "owner" },
"reason": "",
"metadata": { "workspace_slug": "my-team" },
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"workspace_id": "d84c6f21-7a30-4e59-b6c8-1f9d5a2e7043",
"project_id": null,
"created_at": "2026-01-14T09:22:41.478363Z"
}
],
"next_cursor": "b3A9MTcx",
"has_more": true,
"pagination": {
"style": "cursor"
}
}Exporting the trail
Page through it with the cursor envelope, oldest first, and store event_id as the deduplication key:
import requests
BASE = "https://api.plane.so/api/v2/workspaces/my-team/audit-logs/"
params = {"paginate": "cursor", "per_page": 200, "order_by": "created_at"}
cursor = None
while True:
if cursor:
params["cursor"] = cursor # the token carries position only — keep every other param
page = requests.get(BASE, headers={"X-Api-Key": "your-api-key"}, params=params).json()
for entry in page["data"]:
ingest(entry) # key on entry["event_id"]
if not page["has_more"]:
break
cursor = page["next_cursor"]Resume an incremental export with created_after set to the created_at of the last entry you stored, and use event_id to drop the duplicates where the two runs overlap.
Investigating a sudden wave of 409s
409 work_item_types_managed_at_workspace and work_item_types_managed_at_project mean a client is writing to the wrong surface, usually because an admin switched the workspace's work item type mode. Query ?category=settings around the time the errors started to find the change and who made it. See Work item type modes.
Related
- Get an audit log
- Audit logs overview: every field and enum value
- Pagination

