Skip to content

List project members ​

GET/api/v2/workspaces/{slug}/projects/{project_id}/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:requiredstring

The 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:optionalstring

Return 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:optionalstring

A search term matched against the member's user record, so you can find someone by name or email without expanding first.

Expansion ​

expand:optionalstring

Comma-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:optionalstring

Field to sort by. Prefix with - for descending.

  • created_at , -created_at — when the person was added to the project
  • id , -id

Pagination ​

per_page:optionalinteger

Page size. Defaults to 50, maximum 200. Most project rosters fit in a single page.

offset:optionalinteger

Number of rows to skip from the start of the result set. Maximum 10000. Read next from the response rather than computing offsets yourself.

paginate:optionalstring

Set to cursor for the COUNT-free keyset envelope, which returns next_cursor and has_more instead of next and total_count.

count:optionalboolean

Defaults to true. Set to false to skip the COUNT(*) behind total_count; the field is then omitted from the response.

Response shaping ​

fields:optionalstring

Comma-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 ​

projects.members:read

Errors ​

StatusCodeCause
401unauthorizedMissing or invalid credentials.
402payment_requiredThe feature this endpoint belongs to isn't enabled on your plan, or is switched off.
403forbiddenYour role or token scope can't read this project's roster.
404not_foundNo such workspace or project, or it's outside your tenant.
406not_acceptableThe Accept header asks for a representation the API can't produce.
429rate_limitedThrottled. 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.

List project members
bash
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"
Response200
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"
  }
}
Response404
json
{
  "type": "not_found",
  "code": "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.