Skip to the documentation
API

List members

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

GET/members

Runs the real call on your workspace.

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.nextmail.shexport AUTH="Authorization: Bearer $NEXTMAIL_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 exception is addresses:all. When permissions holds it, the person reaches every address on every domain of the workspace, including ones added later, and sends as any of them when permissions also holds emails:send. addresses and domains still list only their direct grants, which they may have kept from before or been given since, so read permissions first: with addresses:all in it they reach everything whatever those arrays hold, and without it they reach only what the arrays list.

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": "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.

Paged like every other list: limit and cursor in, hasMore and nextCursor out. The owner is always the first row of the first page.

Reference