List audit logs
Query the workspace's audit trail. This is the compliance and security-investigation endpoint: scope it to a date range for a review period, to an actor for an access certification, or to outcome=failure when you are looking for attempts that were turned away.
It is also the export endpoint. With ?paginate=cursor you can walk months of history into a SIEM or warehouse without paying for 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 looks empty.
Query Parameters — who and what
actor_id:optionalstringReturn only what one user did. This is the same id as member_id on the member rosters, so an access certification is a roster read followed by one call per person.
Entries with no actor — system and anonymous events — are excluded by any actor_id filter, so run a second unfiltered query if you are asked to account for everything in a window.
event_name:optionalstringReturn only one specific event, for example member.role_updated. Use it when you already know the action you are hunting for; use category when you want a whole class of them.
category:optionalstringReturn only one family of events: auth, member, role, settings, integration, webhook, security, or instance.
role and member answer "who gained access and when". settings answers "what changed about this workspace". auth plus outcome=failure answers "who tried to get in".
outcome:optionalstringReturn only entries with this outcome: success or failure. Failed attempts are recorded, which is what makes this filter 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. ?target_type=…&target_id=… is the "everything that ever happened to this thing" query.
ip_address:optionalstringReturn only entries recorded from this client IP. Start from a suspicious entry, then pivot on its ip_address to see everything else that address did.
search:optionalstringA free-text search term across the entry. Reach for it when you have a name or a fragment rather than 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 — which an audit trail exceeds quickly, so use cursor pagination for anything deeper than a few pages.
paginate:optionalstringSet to cursor for the COUNT-free keyset envelope, which returns next_cursor and has_more instead of next and total_count. This is the mode to use for a full export: it has no offset ceiling and it does not skip or repeat rows as new entries land mid-walk.
count:optionalbooleanDefaults to true. Set to false to skip the COUNT(*) behind total_count; the field is then omitted. Worth doing on every page of an offset walk — cursor pagination never runs a COUNT and ignores this parameter.
Scopes
workspaces.audit_logs:read
Errors
| Status | Code | Cause |
|---|---|---|
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't read this workspace's audit logs. |
404 | resource_not_found | No such workspace, or it's outside your tenant. |
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. Scope the tokens you issue for exports to exactly this, keep them out of user-facing clients, and treat the exported data as it deserves.
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
Walk 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 let event_id absorb the overlap at the boundary.
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 every enum value
- Pagination

