# List a team's members

> One team's memberships, a page at a time, with each person's team role, company role and other teams.

Canonical page: https://www.rundiffusion.com/docs/api/teams/members
Endpoint: GET /api/v2/teams/{team_id}/members
Authorization: OAuth device flow or Company API Access Token

---

One team's members, a page at a time, with each person's role on the team,
their company role, and the other teams they are on.

[Get a team](/docs/api/teams/get) returns a team's whole membership in one
response, including the invitation link for each pending member. This endpoint
is the one to page, search and filter a team's people with, and it names their
roles. It does not return invitation links.

`GET /api/v2/teams/{team_id}/members`

Authorization: OAuth device flow or Company API Access Token

## Who may read it

Whoever manages the company's teams, and the team's own members:

| The caller | Result |
| --- | --- |
| `MANAGE_TEAMS` on the team's company | Any team in the company |
| A member who has joined the team | This team |
| Someone who has joined only other teams in the company | `403 PERMISSION_DENIED` |
| Anyone else, including someone on no team in the company | `404 TEAM_NOT_FOUND` |

A **Company API Access Token** acts as the member it was made for, so it reads
what that member would read with their own sign-in.

`MANAGE_TEAMS` comes from the caller's **company role**, which is a separate
grant from their team role. See
[Two roles, two maps](/docs/api/me#two-roles-two-maps).

## Request

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Yes | Bearer token. This endpoint accepts an OAuth device flow token or a Company API Access Token. A Personal API Access Token is refused. See [Authentication](/docs/api/authentication). |

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `team_id` | string | Yes | The team whose members to list, from [List teams](/docs/api/teams/list). It also names the company, so no separate selector is needed. A team in another company returns 404. |

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `company_id` | string | No | Optional. The team already names its company. Send it to make sure the team belongs to the company you expect; a team in a different one returns 404. |
| `cursor` | string | No | Opaque pagination token. When a response has has_more true, pass its next_cursor here to get the following page. Do not construct or parse one. |
| `limit` | integer | No | Members per page, from 1 to 100. Default: `24` |
| `sort` | string | No | Result order. joined is when the person accepted, and pending invitations sort last in either direction because they have no joined instant. invited is when the invitation was created. Ties always break on the email address, so two pages never overlap. Default: `email` One of: `email`, `joined`, `invited` |
| `order` | string | No | Which direction sort runs in. Default: `asc` One of: `asc`, `desc` |
| `q` | string | No | Case-insensitive substring match on the email address, up to 200 characters. Percent and underscore match themselves rather than acting as wildcards. |
| `status` | string | No | joined for members who have accepted, pending for invitations still outstanding. Omit it for both. One of: `joined`, `pending` |
| `team_role_id` | string | No | Only members whose team_role is this role. |

cURL:

```bash
curl "https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?status=pending" \
  -H "Authorization: Bearer $RUNDIFFUSION_TOKEN"
```

JavaScript:

```javascript
const page = await fetch(
  'https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?status=pending',
  { headers: { Authorization: `Bearer ${process.env.RUNDIFFUSION_TOKEN}` } },
).then(r => r.json());
```

Python:

```python
page = requests.get(
    f"{BASE}/teams/{team_id}/members",
    params={"status": "pending"},
    headers=HEADERS,
).json()
```

The path `team_id` names the team to read. It is not the account selection a
`team_id` query parameter performs on other endpoints.

## Response

`200 OK` with one page of memberships, sorted by email address unless you asked
otherwise.

```json
{
  "data": [
    {
      "key": "sam@example.com",
      "email": "sam@example.com",
      "user_id": "k3PqV9…",
      "invited": "2026-03-14T09:03:40+00:00",
      "joined": "2026-03-14T09:04:52+00:00",
      "is_pending": false,
      "company_role_invite_only": false,
      "team_role": { "id": "Rk7Vq2…", "name": "Editor" },
      "company_role": { "id": "Hn2Lw8…", "name": "Admin" },
      "teams": [
        { "id": "2D8NEx…", "name": "Design Studio", "pending": false, "is_active": true },
        { "id": "7QmWb2…", "name": "Archive 2025", "pending": false, "is_active": false }
      ]
    },
    {
      "key": "new_hire@example.com",
      "email": "new_hire@example.com",
      "user_id": null,
      "invited": "2026-05-02T11:20:07+00:00",
      "joined": null,
      "is_pending": true,
      "company_role_invite_only": false,
      "team_role": { "id": "Pz4Tc1…", "name": "Viewer" },
      "company_role": null,
      "teams": [
        { "id": "2D8NEx…", "name": "Design Studio", "pending": true, "is_active": true }
      ]
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

| Name | Type | Description |
| --- | --- | --- |
| `data` | array | The memberships on this page, one row each. Empty when nobody matches, which is not an error. |
| `next_cursor` | string \| null | Pass this back as cursor to fetch the next page. Null on the last page. |
| `has_more` | boolean | Whether more pages exist beyond this one. This is the flag to branch on when paging. |

### Member object

| Name | Type | Description |
| --- | --- | --- |
| `key` | string | The row's identity, which is the email address. It is also the id of the membership record. Use it to key a list, not user_id, which a pending member does not have yet. |
| `email` | string | The email address the membership belongs to. |
| `user_id` | string \| null | The member's user ID. Always null while is_pending is true, since there is no user until someone accepts. |
| `invited` | string (ISO 8601) \| null | When the invitation was created, in UTC. Present whether or not it has been accepted. |
| `joined` | string (ISO 8601) \| null | When the person joined this team, in UTC, or null while the invitation is pending. |
| `is_pending` | boolean | Whether the invitation is still outstanding. Derived from the joined instant rather than a separate flag, so the two cannot disagree. |
| `company_role_invite_only` | boolean | True when this invitation was created to carry a company role to an address with no account yet, rather than to put the person on this team. Cancelling the company role withdraws it. |
| `team_role` | object \| null | The person's role on this team, with id and name. See [Roles](#roles). |
| `company_role` | object \| null | The person's company role, with id and name. See [Roles](#roles). |
| `teams` | array | Every team in the company this person is on, this one included, each with id, name, pending and is_active. Only the teams you can see. |

### Roles

Both roles are objects with an `id` and a `name`, or `null` for none.

- `team_role` is the role the person holds on this team once they have joined.
  While the invitation is pending, it is the role the invitation will grant when
  they accept.
- `company_role` is the person's company role once they have an account. For
  someone who has not accepted yet, it is the company role their invitation will
  grant.

A role whose definition has been deleted reads as `null`. The two are separate
grants: a team role decides what a member may do inside this team, a company
role what they may do to the organization. See
[Two roles, two maps](/docs/api/me#two-roles-two-maps).

### Other teams

`teams` lists every team in the company the person is on, this one included.
Like everything here it stays inside what you can see: a holder of
`MANAGE_TEAMS` gets all of them, and a member gets the teams they share with
the person.

## Freshness

This listing is read from a copy of the data that trails live changes by up to
about 20 seconds, so someone invited a moment ago may not appear yet.
[Get a team](/docs/api/teams/get) reads live.

## Paging

Cursor paginated, the same way as every other listing. Read `has_more`, and when
it is true send `next_cursor` back as `cursor`.

A cursor belongs to the listing it was issued for: the team, the sort, the order
and every filter. Change any of them partway through a walk and the cursor
returns `400 INVALID_CURSOR`. `limit` may change between pages.

> **This response is a list of people's email addresses**
>
> Treat a page of this endpoint the way you would treat any export of personal
> data: keep it out of shared logs and analytics, and do not cache it anywhere
> your own access controls do not reach.

## Errors

**Status codes**

| Name | Type | Description |
| --- | --- | --- |
| `400` | INVALID_REQUEST | A query parameter failed validation, such as a limit above 100 or a sort this endpoint does not offer. details names the field. |
| `400` | INVALID_CURSOR | The cursor no longer matches this listing, which happens if you change the sort, the order or a filter partway through a walk. Restart the walk without a cursor. |
| `401` | TOKEN_INVALID | Missing or unrecognized token, or a Personal API Access Token, which this endpoint refuses because it is not scoped to a company. |
| `401` | TOKEN_EXPIRED | The OAuth token has expired. Refresh it and retry. |
| `403` | PERMISSION_DENIED | The caller has joined another team in the company, but not this one, and does not hold MANAGE_TEAMS. Someone on no team in the company gets 404 instead. |
| `404` | TEAM_NOT_FOUND | No such team, it belongs to another company, or the caller is on no team in its company and does not hold MANAGE_TEAMS there. The cases are deliberately indistinguishable. |
| `429` | RATE_LIMITED | Too many requests. See [Rate limits](/docs/api/rate-limits#teams). |

A team in another company, or in one where you are on no team, returns `404`,
not `403`, so the response does not confirm that a team you have no part in
exists.
