Skip to content

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.

GEThttps://api2.rundiffusion.com/api/v2/members
AcceptsOAuth device flowCompany API Access Token

Who 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 callerWhat comes back
A Company API Access Token, or MANAGE_COMPANYEveryone in the company, including company-role holders who are on no team
MANAGE_TEAMSEveryone on any team in the company
A member with neither permissionThe people they share a team with, and only the shared teams
Someone who holds nothing in the company403 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…
limitintegeroptionaldefault 24
People per page, from 1 to 100.https://api2.rundiffusion.com/api/v2/members?company_id=Cm4pQ7…&limit=50
sortstringoptionaldefault email
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
orderstringoptionaldefault asc
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"

Three parameters that look alike

This endpoint is the one place three team-shaped names meet, and they answer different questions:

ParameterQuestion it answers
company_idWhich company's people to read
team_idWhich account you are acting as
in_team_idWhich 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:

bash
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:

bash
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.

json
{
  "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:

bash
# 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: 2

status 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:

bash
# 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:

bash
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.

View as Markdown