List workspace 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:requiredstringThe 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:optionalstringReturn 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:optionalstringA search term matched against the member's user record, so you can find someone by name or email without expanding first.
Expansion
expand:optionalstringSet 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:optionalstringField to sort by. Prefix with - for descending.
created_at,-created_at— when the person joined the workspaceid,-id
There is no ordering by name or role; sort the page client-side, or expand and sort on display_name.
Pagination
per_page:optionalintegerPage size. Defaults to 50, maximum 200.
offset:optionalintegerNumber of rows to skip from the start of the result set. Maximum 10000. Read next from the response rather than computing offsets yourself.
paginate:optionalstringSet 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:optionalbooleanDefaults 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
| Status | Code | Cause |
|---|---|---|
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't read this workspace roster. |
404 | resource_not_found | No such workspace, or it's outside your tenant. |
429 | rate_limited | Throttled. 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.
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"import requests
response = requests.get(
"https://api.plane.so/api/v2/workspaces/my-team/members/",
headers={"X-Api-Key": "your-api-key"},
params={"expand": "member", "per_page": 50},
)
print(response.json())const params = new URLSearchParams({ expand: "member", per_page: "50" });
const response = await fetch(`https://api.plane.so/api/v2/workspaces/my-team/members/?${params}`, {
headers: {
"X-Api-Key": "your-api-key",
},
});
const data = await response.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"
}
}{
"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.

