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.
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 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.
https://api2.rundiffusion.com/api/v2/teams/{team_id}/membersWho 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.
Request
Headers
AuthorizationstringRequired- 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.
Authorization: Bearer eyJhbGciOi…
Path parameters
team_idstringRequired- The team whose members to list, from List teams. It also names the company, so no separate selector is needed. A team in another company returns 404.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members
Query
company_idstringoptional- 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.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?company_id=Cm4pQ7… cursorstringoptional- 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.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?cursor=eyJ2IjoxL… limitintegeroptionaldefault24- Members per page, from 1 to 100.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?limit=100 sortstringoptionaldefaultemail- 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.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?sort=joined&order=desc orderstringoptionaldefaultasc- Which direction sort runs in.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?sort=invited&order=desc qstringoptional- Case-insensitive substring match on the email address, up to 200 characters. Percent and underscore match themselves rather than acting as wildcards.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?q=example.com statusstringoptional- joined for members who have accepted, pending for invitations still outstanding. Omit it for both.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?status=pending team_role_idstringoptional- Only members whose team_role is this role.
https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?team_role_id=Rk7Vq2…
curl "https://api2.rundiffusion.com/api/v2/teams/2D8NEx…/members?status=pending" \
-H "Authorization: Bearer $RUNDIFFUSION_TOKEN"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());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.
{
"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
}dataarray- The memberships on this page, one row each. Empty when nobody matches, which is not an error.
next_cursorstring | null- Pass this back as cursor to fetch the next page. Null on the last page.
has_moreboolean- Whether more pages exist beyond this one. This is the flag to branch on when paging.
Member object
keystring- 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.
emailstring- The email address the membership belongs to.
user_idstring | null- The member's user ID. Always null while is_pending is true, since there is no user until someone accepts.
invitedstring (ISO 8601) | null- When the invitation was created, in UTC. Present whether or not it has been accepted.
joinedstring (ISO 8601) | null- When the person joined this team, in UTC, or null while the invitation is pending.
is_pendingboolean- Whether the invitation is still outstanding. Derived from the joined instant rather than a separate flag, so the two cannot disagree.
company_role_invite_onlyboolean- 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_roleobject | null- The person's role on this team, with id and name. See Roles.
company_roleobject | null- The person's company role, with id and name. See Roles.
teamsarray- 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_roleis 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_roleis 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.
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 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.
Errors
Status codes
400INVALID_REQUEST- A query parameter failed validation, such as a limit above 100 or a sort this endpoint does not offer. details names the field.
400INVALID_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.
401TOKEN_INVALID- Missing or unrecognized token, or a Personal API Access Token, which this endpoint refuses because it is not scoped to a company.
401TOKEN_EXPIRED- The OAuth token has expired. Refresh it and retry.
403PERMISSION_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.
404TEAM_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.
429RATE_LIMITED- Too many requests. See Rate limits.
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.
