List project members
Return one project's active member roster as a paginated list. Reach for this when you need the people who can actually be assigned work in a project — the workspace roster is wider, and includes members with no access to this project.
Roles here are project roles, so the same person can come back as member on the workspace roster and as contributor on this one.
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.
project_id:requiredstring (uuid)The project whose roster you want. A project id from another workspace returns 404.
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 member_id__in to check several at once, comma-separated.
Pairing ?member_id=<user> with ?per_page=1 is the cheapest membership check there is: an empty data array means that user is not on this project.
role:optionalstringReturn only members holding this role slug, for example ?role=contributor. Use role__in for several roles at once, comma-separated.
The value is a plain string, not a fixed enum — a custom role is matched by its 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; 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 was added to the projectid,-id
Pagination
per_page:optionalintegerPage size. Defaults to 50, maximum 200. Most project rosters fit in a single page.
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.
count:optionalbooleanDefaults to true. Set to false to skip the COUNT(*) behind total_count; the field is then omitted from the response.
Scopes
projects.members:read
Errors
| Status | Code | Cause |
|---|---|---|
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Your role or token scope can't read this project's roster. |
404 | resource_not_found | No such workspace or project, or it's outside your tenant. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
Validate assignees against this list, not the workspace one
A member_id that is on the workspace roster but not on this project is not a valid assignee for the project's work items. Check membership here before writing assignee_ids.
A 409 is not a permission signal
403 means the caller's role or scope is too narrow. 409 work_item_types_managed_at_workspace or work_item_types_managed_at_project means the write belongs on the other surface, whatever the caller's role is. See Work item type modes.
curl -X GET \
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/members/?expand=member" \
-H "X-Api-Key: $PLANE_API_KEY"import requests
response = requests.get(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/members/",
headers={"X-Api-Key": "your-api-key"},
params={"expand": "member"},
)
print(response.json())const params = new URLSearchParams({ expand: "member" });
const response = await fetch(
`https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/members/?${params}`,
{
headers: {
"X-Api-Key": "your-api-key",
},
}
);
const data = await response.json();{
"data": [
{
"id": "9d2f5c81-4b73-4e60-8a1f-2c9b6d3e7f04",
"member_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"role": "contributor",
"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": "b5c07e29-1d84-4f36-9a72-6e13c8d5a0f7",
"member_id": "7f2b9e04-6c1d-4a58-9e3b-0d4c8a2f6b71",
"role": "release-manager",
"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"
}
}{
"type": "https://api.plane.so/errors/resource_not_found",
"title": "Not Found",
"status": 404,
"code": "resource_not_found",
"detail": "No Project matches the given query."
}Custom roles appear as their own slug
release-manager above is a custom role defined by the workspace. role has no enum in the schema, so treat any slug you do not recognize as a role you have no rules for rather than as bad data.

