Skip to content

List audit logs

GET/api/v2/workspaces/{slug}/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:requiredstring

The 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:optionalstring

Return 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:optionalstring

Return 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:optionalstring

Return 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:optionalstring

Return 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:optionalstring

Return 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:optionalstring

Return only entries acting on this specific object. ?target_type=…&target_id=… is the "everything that ever happened to this thing" query.

ip_address:optionalstring

Return 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:optionalstring

A 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:optionalstring

Field to sort by. Prefix with - for descending.

  • -created_at , created_at — newest first, or oldest first
  • id , -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:optionalinteger

Page size. Defaults to 50, maximum 200. Use 200 for exports.

offset:optionalinteger

Number 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:optionalstring

Set 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:optionalboolean

Defaults 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

StatusCodeCause
401unauthorizedMissing or invalid credentials.
403forbiddenYour role or token scope can't read this workspace's audit logs.
404resource_not_foundNo such workspace, or it's outside your tenant.
429rate_limitedThrottled. 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.

List audit logs
bash
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"
Response200
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"
  }
}
Response200
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"
    }
  ],
  "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:

python
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.