List workspace members
Return the workspace's active member roster as a paginated list. Use it to resolve a person to the member_id you assign work to, or poll it 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 falls back to the 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.
member_id:optionalstring (uuid)Return the membership for one user. Use member_id__in to look up several users at once, comma-separated, for example ?member_id__in=16c61a3a-512a-48ac-b0be-b6b46fe6f430,7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71.
This is a cheap way to check whether a person is still in the workspace, and their current role, without paging through 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, so 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:optionalstringComma-separated relations to embed alongside the ids: member (the member's user object).
Expanded objects get their own key: ?expand=state keeps state_id and adds a state object next to it, so an id is never replaced by an object. An unknown value returns 400.
?fields= and ?expand= take separate sets of names. Relation names aren't valid in ?fields=, field names aren't valid in ?expand=, and field filtering never removes an expanded object: ?fields=id,name&expand=state returns id, name and state. 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 keyset envelope (no COUNT query), which returns next_cursor and has_more instead of next and total_count. Use it when you page through a large workspace from start to finish.
count:optionalbooleanDefaults to true. Set to false to skip the COUNT(*) behind total_count; the field is then omitted from the response.
Response shaping
fields:optionalstringComma-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: id, member_id, role.
See Sparse fields.
Scopes
workspaces.members:read
Errors
| Status | Code | Cause |
|---|---|---|
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | The feature this endpoint belongs to isn't enabled on your plan, or is switched off. |
403 | forbidden | Your role or token scope can't read this workspace roster. |
404 | not_found | No such workspace, or it's outside your tenant. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
Membership id versus user id
id identifies the membership row, and member_id identifies the person. Key caches and joins on member_id, because the same user has a different id on every roster they appear in. See Members overview.
Roles restrict writes, not this read
A role that is too narrow returns 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. 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.

