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:optionalstringComma-separated relations to embed alongside the ids: member (the member's user object).
Expansion is separate-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 is a 400.
?fields= and ?expand= are independent namespaces. Relation names are not valid ?fields= tokens (and vice versa), and an expanded object survives field filtering — ?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 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.
Response shaping
fields:optionalstringComma-separated list of fields to return on each row. Unrequested keys are omitted from the response, not returned as null, so absent means "not requested" and null means "actually null". id always comes back whether or not you name it.
Pass all for every requestable field. An unknown name is a 400 that lists the valid set and suggests the closest match, so a typo can't silently cost you the saving.
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; 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.

