---
title: "List members"
description: "Everybody in the workspace, the role each holds, and the addresses each was given."
url: "https://openemail.uk/docs/api/members/list"
area: "API"
category: "Roles & access"
---

# List members

Everybody in the workspace, the role each holds, and the addresses each was given.

`GET /members`

## GET /members

Everybody in the workspace, the role each holds, and the addresses each was given.

## A member is two grants, not one

**shell**

```
export OE=https://api.openemail.uk
export AUTH="Authorization: Bearer $OPENEMAIL_API_KEY"
```

`role` is what they may DO: one row, one role, the same object `/roles` describes. `addresses` is what they may do it TO: one entry per address, each carrying its own `access`. A client must not collapse them: a role holding `emails:send` with an empty `addresses` array is somebody who may send from nothing, and a full `addresses` array under a viewer role is somebody who may send from nothing either. The send path checks both, and a screen showing one of them will confidently explain the wrong refusal.

The address rows say `access` where the stored column says `role`, and the rename is the point rather than a tidy-up: this object already has a `role` field meaning something else entirely, and two `role`s one nesting level apart holding values from two different vocabularies is a bug waiting for the first person who reads it quickly. `access` is `member`, which reads the address and sends as it, or `viewer`, which only reads it.

> `implied: true` means NOBODY CHOSE THIS ROLE. Sharing shipped long before roles did, so most people with access to a mailbox hold address grants and no member row at all; rather than deny them their mail until a backfill has run, the service infers a built-in from the widest grant they hold and reports it with a null `role.id`. Show that as “implied by access” rather than as a role somebody picked. Until a `PATCH` turns the inference into a decision, widening their address access silently widens what they may do.

The OWNER is the FIRST row, marked `isOwner: true`, with `role.builtin` of `owner`. They are the account the workspace is keyed on, they hold every permission by definition, and `POST`, `PATCH` and `DELETE` all refuse them with `member_is_owner`. So an unshared workspace reports one member rather than none. Count seats by excluding `isOwner`.

## Example

Needs `members:read`. The owner comes first, then everybody else by email rather than by when they joined, because this list is read to find one person rather than to see what changed.

**curl**

```
curl "$OE/members" -H "$AUTH"
```

**Response**

```
{
  "object": "list",
  "data": [
    {
      "object": "member",
      "userId": "nQ8vBz1aRd4tYwKx7fQ2mN8vBz1aRd4t",
      "email": "sam@acme.com",
      "name": "Sam Okonjo",
      "image": null,
      "role": {
        "id": "role_2b81de079c1f0a4b7e05d386",
        "name": "Support",
        "builtin": null
      },
      "implied": false,
      "permissions": [
        "emails:send",
        "emails:read",
        "threads:read",
        "threads:write",
        "labels:read",
        "labels:write",
        "contacts:read"
      ],
      "addresses": [
        {
          "addressId": "2b81de07-9c1f-4a4b-8e05-d3862c1f0a44",
          "address": "help@acme.com",
          "access": "member"
        },
        {
          "addressId": "c40a95f2-1cc6-4d31-82a8-9e075d31c2a8",
          "address": "billing@acme.com",
          "access": "viewer"
        }
      ],
      "createdAt": "2026-08-12T14:20:00.000Z"
    },
    {
      "object": "member",
      "userId": "7fQ2mN8vBz1aRd4tYwKx7fQ2mN8vBz1a",
      "email": "wren@acme.com",
      "name": null,
      "image": null,
      "role": { "id": null, "name": "Viewer", "builtin": "viewer" },
      "implied": true,
      "permissions": [
        "emails:read",
        "drafts:read",
        "threads:read",
        "labels:read",
        "contacts:read",
        "calendar:read",
        "templates:read",
        "rules:read",
        "connections:read",
        "settings:read"
      ],
      "addresses": [
        {
          "addressId": "c40a95f2-1cc6-4d31-82a8-9e075d31c2a8",
          "address": "billing@acme.com",
          "access": "viewer"
        }
      ],
      "createdAt": null
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

> Two populations in one list, and it has to be: somebody can hold a role and no address, and somebody can hold an address and no role row. Listing only the overlap would hide both, and on most workspaces the second group is the larger one.

> `createdAt` is null for somebody who has grants but has never had a member row written, the same people `implied` is true for. It is when they were given a ROLE, not when they were first shared an address.

> `permissions` is the flat resolved list rather than a set of booleans. A client asking “does this include `templates:write`” cannot fall behind the vocabulary; a client handed `{ canEditTemplates: true }` silently can.

> Uncursored, with the standard envelope. A workspace’s membership is bounded by how many people its owner has actually shared it with, and paginating that would be ceremony in front of something a client fetches once and renders whole.
