Members overview
A member record is the link between a user and a workspace or a project. A roster tells you who belongs there and in what role.
v2 returns the membership, not the user. Every row has three fields: the membership's own id, the member_id of the user it points at, and the role that user holds in this context. Names, emails, and avatars belong to the user. To include them in the same response, use ?expand=member.
The member object
Attributes
idstring (uuid)Unique identifier for the membership row, not for the user. The same person has one membership in the workspace and another in each project they belong to, so they have several
idvalues but onemember_id.member_idstring (uuid)The user this membership belongs to. This is the id that lines up with
created_by_idon other objects,assignee_idson work items, andactor_idin audit logs. Join onmember_id, never onid.rolestringThe member's role in this workspace or project, as a slug. It decides whether this person can write here.
The slug depends on context: the same person can be
memberon the workspace roster andcontributoron a project roster. The field is nullable.nullappears only on legacy rows that predate role slugs.
Roles are an open set, not an enum
The schema types role as a plain string with no enum. Besides built-in slugs such as owner, member, and contributor, a workspace can define custom roles, each with its own slug. Match the slugs you care about and handle any other value with a default case. Do not write an exhaustive switch.
{
"id": "c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40",
"member_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"role": "member"
}{
"id": "c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40",
"member_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"role": "member",
"member": {
"id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"display_name": "Priya Raghavan",
"avatar_url": "https://assets.plane.so/avatars/16c61a3a.png",
"email": "priya@example.com"
}
}Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v2/workspaces/{slug}/projects/{project_id}/members/ | List project members |
GET | /api/v2/workspaces/{slug}/members/ | List workspace members |
GET | /api/v2/workspaces/{slug}/projects/{project_id}/members/{pk}/ | Get a project member |
GET | /api/v2/workspaces/{slug}/project-role-distribution/ | Get project role distribution |
POST | /api/v2/workspaces/{slug}/projects/{project_id}/members/ | Create a project member |
PATCH | /api/v2/workspaces/{slug}/projects/{project_id}/members/{pk}/ | Update a project member |
DELETE | /api/v2/workspaces/{slug}/projects/{project_id}/members/{pk}/ | Delete a project member |
POST | /api/v2/workspaces/{slug}/members/remove/ | Remove a workspace member |
Both list endpoints return the active roster for their scope. The workspace list is the superset: a project member is always a workspace member too, so a member_id on a project roster also appears on the workspace roster, usually with a different role.
Membership is writable in v2. Add, change, and remove project memberships with Create a project member, Update a project member, and Delete a project member. Remove someone from the workspace with Remove a workspace member. To bring in someone who is not in the workspace yet, send them an invitation. The v1 members endpoints remain available.
Membership ids are not user ids
Design your integration around this distinction. For the same person:
| Roster | id | member_id |
|---|---|---|
Workspace my-team | c1b7e4a9-2f66-4a1d-9c3b-7d5e2f8a1b40 | 16c61a3a-512a-48ac-b0be-b6b46fe6f430 |
Project 4af68566-…067b | 9d2f5c81-4b73-4e60-8a1f-2c9b6d3e7f04 | 16c61a3a-512a-48ac-b0be-b6b46fe6f430 |
Key your user cache on member_id. Use id only when you need to talk about the membership itself.
Expanding the user
Members are one of only two v2 resources that accept ?expand=. The other is work items. The only allowed value here is member.
GET /api/v2/workspaces/my-team/members/?expand=memberExpansion adds a separate key: member_id stays where it was and a member object is added beside it. The id is never swapped for an object, so code that reads member_id keeps working whether or not the caller expanded. The expanded object carries id, display_name, avatar_url, and email.
Any other value, such as user, project, or role, returns 400 invalid_request. See Expanding relations for the full contract.
Roles and errors
A role that is too narrow returns 403 forbidden on the write you attempted. Roles never cause a 404: a workspace or project outside your tenant returns 404 not_found whether or not you would have had the role for it.
A 409 is never a role problem
If a write comes back 409 work_item_types_managed_at_workspace or work_item_types_managed_at_project, the caller's role and scopes are fine. The workspace manages work item types on the other surface, and the same call succeeds there. See Work item type modes.
Changed from v1
- A member row is now the membership, not a flattened user. v1 returned
first_name,last_name,email,avatar,avatar_url, anddisplay_nameinline; v2 returnsid,member_id, androle, and moves the user fields behind?expand=member. roleis a slug string, not the numeric level v1 returned. Comparerole == "member", notrole >= 15.- Responses are paginated envelopes rather than bare arrays. See Pagination.
- v2 exposes reads only. The v1 add, update, and remove member endpoints have no v2 equivalent yet.
See Migrating from v1 for the full list.
Related
- List workspace members
- List project members
- Expanding relations
- Get current user: the
member_idyour own credentials map to

