Skip to the documentation
API

List members

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

GETapi.openemail.uk/members

Runs the real call against your workspace, with your own key.

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.ukexport 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 roles 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": "[email protected]",      "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": "[email protected]",          "access": "member"        },        {          "addressId": "c40a95f2-1cc6-4d31-82a8-9e075d31c2a8",          "address": "[email protected]",          "access": "viewer"        }      ],      "createdAt": "2026-08-12T14:20:00.000Z"    },    {      "object": "member",      "userId": "7fQ2mN8vBz1aRd4tYwKx7fQ2mN8vBz1a",      "email": "[email protected]",      "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": "[email protected]",          "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.