Skip to content

List workspace members

GET/api/v2/workspaces/{slug}/members/

Return the workspace's active member roster as a paginated list. This is the endpoint that resolves a person to the member_id you assign work to, and the one you poll to keep a user directory in sync with Plane.

The workspace roster is the superset of every project roster — anyone on a project is on this list too.

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.

Query Parameters

Filters combine with AND. Check your spelling on order_by and paginate — neither is validated. An unrecognized order_by value silently falls back to the default ordering, and anything other than paginate=cursor silently uses offset pagination. A typo shows up as an unexpected sort order or envelope, not as an error.

member_id:optionalstring (uuid)

Return the membership for one user. Use the member_id__in variant to look several users up at once, comma-separated — ?member_id__in=16c61a3a-512a-48ac-b0be-b6b46fe6f430,7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71.

This is the cheap way to answer "is this person still in the workspace, and what is their role now?" without paging the whole roster.

role:optionalstring

Return only members holding this role slug, for example ?role=owner. Use role__in for several roles at once, comma-separated.

The value is a plain string, not a fixed enum — custom roles are matched by their own slug.

search:optionalstring

A search term matched against the member's user record, so you can find someone by name or email without expanding first.

Expansion

expand:optionalstring

Set to member to embed the user beside the id. member_id stays in place and a member object with id, display_name, avatar_url, and email is added next to it.

member is the only accepted value here; anything else returns 400. See Expanding relations.

Ordering

order_by:optionalstring

Field to sort by. Prefix with - for descending.

  • created_at , -created_at — when the person joined the workspace
  • id , -id

There is no ordering by name or role; sort the page client-side, or expand and sort on display_name.

Pagination

per_page:optionalinteger

Page size. Defaults to 50, maximum 200.

offset:optionalinteger

Number of rows to skip from the start of the result set. Maximum 10000. Read next from the response rather than computing offsets yourself.

paginate:optionalstring

Set to cursor for the COUNT-free keyset envelope, which returns next_cursor and has_more instead of next and total_count. Worth it when you are walking a large workspace end to end.

count:optionalboolean

Defaults to true. Set to false to skip the COUNT(*) behind total_count; the field is then omitted from the response.

Scopes

workspaces.members:read

Errors

StatusCodeCause
401unauthorizedMissing or invalid credentials.
403forbiddenYour role or token scope can't read this workspace roster.
404resource_not_foundNo such workspace, or it's outside your tenant.
429rate_limitedThrottled. Honor the Retry-After header before retrying.

Membership id versus user id

id identifies the membership row; member_id identifies the person. Key caches and joins on member_id — the same user has a different id on every roster they appear in. See Members overview.

Roles gate writes, not this read

A narrow role shows up as 403 on the write you attempted. A 409 work_item_types_managed_at_workspace or work_item_types_managed_at_project is not a role problem at all — see Work item type modes.

List workspace members
bash
curl -X GET \
  "https://api.plane.so/api/v2/workspaces/my-team/members/?expand=member&per_page=50" \
  -H "X-Api-Key: $PLANE_API_KEY"
Response200
json
{
  "data": [
    {
      "id": "c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40",
      "member_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "role": "owner",
      "member": {
        "id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
        "display_name": "Priya Raghavan",
        "avatar_url": "https://assets.plane.so/avatars/16c61a3a.png",
        "email": "priya@example.com"
      }
    },
    {
      "id": "3e8a5d17-9c40-4b2f-81d6-4a7f2b9e0c53",
      "member_id": "7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71",
      "role": "member",
      "member": {
        "id": "7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71",
        "display_name": "Devansh Kapoor",
        "avatar_url": null,
        "email": "devansh@example.com"
      }
    }
  ],
  "next": null,
  "previous": null,
  "total_count": 2,
  "pagination": {
    "style": "offset"
  }
}
Response200
json
{
  "data": [
    {
      "id": "c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40",
      "member_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "role": "owner"
    },
    {
      "id": "3e8a5d17-9c40-4b2f-81d6-4a7f2b9e0c53",
      "member_id": "7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71",
      "role": "member"
    }
  ],
  "next": null,
  "previous": null,
  "total_count": 2,
  "pagination": {
    "style": "offset"
  }
}

Expanding a page costs one query, not one per row

?expand=member loads the users in bulk, so a 200-row page is a single extra query. Prefer one expanded page over 200 follow-up requests.