Skip to content

List audit logs ​

GET/api/v2/workspaces/{slug}/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: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 comes back empty.

Query parameters: who and what ​

actor_id:optionalstring

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

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

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

Return 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: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. Use ?target_type=…&target_id=… to get the full history of one object.

ip_address:optionalstring

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

Free-text search across the entry. Use it when you have a name or partial text instead of 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. An audit trail passes that quickly, so use cursor pagination to go deeper than a few pages.

paginate:optionalstring

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

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

Comma-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 ​

StatusCodeCause
401unauthorizedMissing or invalid credentials.
402payment_requiredThe feature this endpoint belongs to isn't enabled on your plan, or is switched off.
403forbiddenYour role or token scope can't read this workspace's audit logs.
404not_foundNo such workspace, or it's outside your tenant.
406not_acceptableThe Accept header asks for a representation the API can't produce.
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. Give export tokens only this scope, keep them out of user-facing clients, and handle the exported data as sensitive.

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 ​

Page through 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 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.