List members
Everyone in a company, one row per person across its teams.
On this page
Everyone in a company, one row per person, with the teams they are on and whether they hold a company role.
List teams and Get a team answer per team, so somebody on four teams appears four times and you assemble the person yourself. This endpoint has already done that: it groups by email address and hands back one row carrying every team you are allowed to see them on.
https://api2.rundiffusion.com/api/v2/membersWho sees what
This endpoint has no single permission gate. What it returns depends on who is asking, and that shapes the response rather than refusing it:
| The caller | What comes back |
|---|---|
A Company API Access Token, or MANAGE_COMPANY | Everyone in the company, including company-role holders who are on no team |
MANAGE_TEAMS | Everyone on any team in the company |
| A member with neither permission | The people they share a team with, and only the shared teams |
| Someone who holds nothing in the company | 403 PERMISSION_DENIED |
So a regular member gets a directory of the colleagues they work alongside, and an administrator gets the whole organization, from the same request.
One consequence is worth building around: membership_count is counted
within your scope, not company-wide, so a colleague you share one team with
reports membership_count: 1 even if they are on ten.
Note that scope is about which teams, never about whether they are activated.
That is is_active, and it applies to everyone alike.
MANAGE_TEAMS and MANAGE_COMPANY come from the caller's company role,
which is a separate grant from their team role. Get identity
returns it under company_permissions, not in the permissions map beside it.
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, because it reaches only its owner's own resources and is not scoped to a company. See Authentication.
Authorization: Bearer eyJhbGciOi…
Query
company_idstringoptional- The company to read. Required with OAuth, which names a person rather than a company. A Company API Access Token already names its company, so it does not need this. Get identity returns it on every TEAM account.
https://api2.rundiffusion.com/api/v2/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/members?company_id=Cm4pQ7…&cursor=2D8NEx… limitintegeroptionaldefault24- People per page, from 1 to 100.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&limit=50 sortstringoptionaldefaultemail- Result order. Under joined, people who are still pending sort last in either direction, because they have no joined instant to order by. membership_count counts only the teams you can see, so it orders by your reading of each person. Ties always break on the email address, so two pages never overlap.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&sort=joined orderstringoptionaldefaultasc- Which direction sort runs in.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&sort=joined&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, so a search for a literal address behaves the way it reads.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&q=example.com emailstringoptional- An exact, case-insensitive match on the address, up to 320 characters. Use this to look one person up. q is a substring match and can return their neighbours as well.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&email=sam@example.com statusstringoptional- How far along a person's invitations are. pending is someone who has accepted none of the ones you can see; joined is someone who has accepted at least one. Omit it for both. A company-role holder who is on no team is neither, so they come back only when you omit it.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&status=pending is_activebooleanoptional- Activation state of the teams a person is counted through. true for activated teams, false for deactivated ones, omitted for both, which is the default and matches List teams. A person is assembled only from the memberships this leaves, so their membership_teams, membership_count and status all follow it.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&is_active=true in_team_idstringoptional- Only people with a membership on this team, accepted or pending. This is a filter, not an account selection, which is why it is not called team_id. A team outside what you can see matches nobody rather than returning an error.
https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&in_team_id=2D8NEx…
curl "https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&limit=25" \
-H "Authorization: Bearer $RUNDIFFUSION_COMPANY_TOKEN"const page = await fetch(
'https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&limit=25',
{ headers: { Authorization: `Bearer ${process.env.RUNDIFFUSION_COMPANY_TOKEN}` } },
).then(r => r.json());page = requests.get(
f"{BASE}/members",
params={"company_id": company_id, "limit": 25},
headers=HEADERS,
).json()Three parameters that look alike
This endpoint is the one place three team-shaped names meet, and they answer different questions:
| Parameter | Question it answers |
|---|---|
company_id | Which company's people to read |
team_id | Which account you are acting as |
in_team_id | Which of those people to show |
team_id is the account selection every endpoint takes, and it is handled
before this one runs: the caller is resolved to that team's account and their
membership of it is checked. It does not narrow the result. Send the team you
are acting under, exactly as you would on any other call.
in_team_id is the filter. It has its own name precisely because team_id is
taken: a filter under that name would be read as an account selection, and an
administrator narrowing to a team they do not belong to would be refused at the
door rather than shown the team.
A Company API Access Token names its company already, so it needs neither
company_id nor team_id:
curl "https://api2.rundiffusion.com/api/v2/members" \
-H "Authorization: Bearer $RUNDIFFUSION_COMPANY_TOKEN"OAuth names a person, who may belong to more than one company, so name the company:
curl "https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…" \
-H "Authorization: Bearer $RUNDIFFUSION_TOKEN"A Personal API Access Token is refused.
Response
200 OK with one page of people, sorted by email address unless you asked
otherwise.
{
"data": [
{
"key": "sam@example.com",
"email": "sam@example.com",
"user_id": "k3PqV9…",
"is_pending": false,
"joined": "2026-03-14T09:04:52+00:00",
"first_invited": "2026-03-14T09:03:40+00:00",
"membership_teams": [
{ "id": "2D8NEx…", "name": "Design Studio", "pending": false, "is_active": true },
{ "id": "7QmWb2…", "name": "Archive 2025", "pending": false, "is_active": false }
],
"membership_count": 2,
"has_company_role": true,
"invite_only_stub": false,
"on_no_team": false
},
{
"key": "new_hire@example.com",
"email": "new_hire@example.com",
"user_id": null,
"is_pending": true,
"joined": null,
"first_invited": "2026-05-02T11:20:07+00:00",
"membership_teams": [
{ "id": "2D8NEx…", "name": "Design Studio", "pending": true, "is_active": true }
],
"membership_count": 1,
"has_company_role": false,
"invite_only_stub": false,
"on_no_team": false
}
],
"next_cursor": null,
"has_more": false
}dataarray- The people 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. Stable across teams, and the id of every membership record the person holds. Use it to key a list, not user_id, which a pending person does not have yet.
emailstring- The email address the memberships belong to.
user_idstring | null- The person's user ID, or null while none of the invitations you can see has been accepted. It is the id other endpoints report a generation under.
is_pendingboolean- True when every membership you can see is an outstanding invitation. Derived from the accepted-at instant rather than a separate flag, so it cannot disagree with joined.
joinedstring (ISO 8601) | null- The earliest accepted-at instant across the memberships you can see, in UTC, or null while is_pending is true.
first_invitedstring (ISO 8601) | null- When the earliest membership you can see was created, in UTC. It is present whether or not anything has been accepted, so it is how you find invitations that have been sitting unanswered. For a person on no team it is when their company role was granted.
membership_teamsarray- The teams this person's memberships are on, each with id, name, pending and is_active. Only the teams you can see, which for an administrator is every team in the company and for everyone else is the teams they share with this person.
membership_countinteger- How many entries membership_teams has. Zero for a person on no team.
has_company_roleboolean- Whether the person holds a company role, which is a separate grant from any team role. The row does not name which role it is.
invite_only_stubboolean- True when every membership you can see was created by granting a company role to an address with no account yet, rather than by inviting that person to the team. Company roles do not belong to a team, so an invitation has to be created to carry one. It is a real invitation and can carry a team role of its own, but cancelling the company role withdraws it entirely, where cancelling a role added onto an ordinary invitation leaves that invitation standing. A team role never produces one of these, because a team role is part of a membership and cannot exist without it.
on_no_teamboolean- True for a company-role holder who is on no team at all. It does not move with is_active, so a person who is on a deactivated team is a member of it, never a role-only row. Only an administrative reading includes such rows at all.
A person is an email address
key is the email address, and it is the field to build on. An invitation that
nobody has accepted has no user behind it yet, so user_id is null and stays
null until someone signs up. The address is what every membership record is
keyed by, what the invitation was sent to, and what the person keeps as they
join more teams.
That also means an address is a person here. Somebody invited under one address who later signs up under a different one is two rows, because nothing in the data ties them together.
Deactivated teams
A team can be deactivated without being deleted, and its memberships stay. By
default this endpoint returns people through both activated and deactivated
teams, the same way List teams returns both when you
omit is_active. Pass is_active=true for the company as it stands today, or
is_active=false to read only what has been archived.
The filter decides which memberships a person is assembled from, so it moves more than the row count:
# Someone on two activated teams and one deactivated one
curl "…/members?company_id=Cm4pQ7…&email=sam@example.com" # membership_count: 3
curl "…/members?company_id=Cm4pQ7…&email=sam@example.com&is_active=true" # membership_count: 2status follows it too: a person whose only accepted membership is on a
deactivated team reads as is_pending: true under is_active=true, because
the invitation they accepted is not in that reading.
Each entry in membership_teams carries its own is_active, so you can show both kinds
at once and mark the archived ones rather than filtering server side.
Two things deliberately do not move with it. Scope is one: which teams you
may see people on is decided by your permissions and your memberships, and a
team you were on is still yours to ask about after it is deactivated. The other
is on_no_team, which means on no team at all. If it followed the filter, then
is_active=false would answer with every company-role holder who is on no
deactivated team, which is most of them.
One consequence to know: a company-role holder whose teams are all
deactivated is in neither half of is_active=true. They have no activated
membership, so they are not a member there, and they are on a team, so they are
not a role-only row either. The default reading and is_active=false both list
them, as the member they are.
Paging
Cursor paginated, the same way as every other listing. Read has_more, and when
it is true send next_cursor back as cursor:
# The first page
curl "https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&limit=50" \
-H "Authorization: Bearer $RUNDIFFUSION_TOKEN"
# The next one
curl "https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&limit=50&cursor=2D8NEx…" \
-H "Authorization: Bearer $RUNDIFFUSION_TOKEN"A cursor belongs to the listing it was issued for: the company, the sort, the
order and every filter. Change any of them partway through a walk and the cursor
returns 400 INVALID_CURSOR rather than quietly resuming somewhere else in a
different result set, so keep them constant for the length of a walk and start
again without a cursor when you change one.
Two things deliberately do not invalidate a cursor. limit may change between
pages, because a cursor is a position and not an offset. And your scope is
re-applied to every request, so a cursor is never a way to reach people you
could not otherwise see.
Every sort breaks ties on the email address, which is what makes the order total, so a person cannot appear on two pages or fall between them.
Finding one person
email matches exactly and is the right way to resolve a known address:
curl "https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&email=sam@example.com" \
-H "Authorization: Bearer $RUNDIFFUSION_TOKEN"q is a substring search for a person-facing search box, so q=sam also finds
sammy@example.com and samira@example.org. Both can be combined with the
other filters.
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.
401UNAUTHENTICATED- Missing, malformed, or expired token. Refresh it and retry.
403PERMISSION_DENIED- The caller holds nothing in the company named, meaning no membership on any of its teams and neither MANAGE_TEAMS nor MANAGE_COMPANY. A Personal API Access Token is refused here too.
429RATE_LIMITED- Too many requests. See Rate limits.
A caller who holds nothing in the company named gets 403, with a message that
says what would grant access. It reads the same whether or not the company
exists, so the response does not answer that question for someone outside it.
