---
title: "List roles"
description: "Every role on the workspace, built-ins first, with how many people and keys hold each."
url: "https://nextmail.sh/docs/api/roles/list"
area: "API"
category: "Roles & access"
---

# List roles

Every role on the workspace, built-ins first, with how many people and keys hold each.

`GET /roles`

## GET /roles

Every role on the workspace, built-ins first, with how many people and keys hold each.

## Two axes, and they are not the same question

**shell**

```
export OE=https://api.nextmail.sh
export AUTH="Authorization: Bearer $NEXTMAIL_API_KEY"
```

A ROLE says what somebody may DO in this workspace: read mail, send it, edit templates, add a domain. A GRANT says which ADDRESSES they may do it to, and lives next door on `/members/{userId}/addresses` as `member` (reads the address and sends as it) or `viewer` (reads it only). Both have to agree before a message goes out: a role holding `emails:send` with no grants can send from nothing, and every address in the workspace under a viewer grant can send from nothing either.

> One permission crosses the two axes. A role holding `addresses:all` reaches every address on every domain of the workspace, including ones added later, with no grant at all, and sends as any of them when it also holds `emails:send`. `/members` still lists only the grants such a member was given directly, which may be none, so read `permissions` before their `addresses` and `domains`: they reach everything whatever those arrays hold. It is console-only, like `workspace:manage`: no key or token can hold it or hand it out, so it is ticked in the app and this API only reads it.

Every workspace is seeded with the same six roles. `Owner`, `Admin`, `Member` and `Viewer` form a ladder. Each holds everything the next one does, so demoting somebody narrows their access rather than swapping it for a different slice. `Developer` and `Billing` are not rungs on it: `Developer` builds integrations (keys, webhooks, templates, sending, the knowledge base and automations) and reads none of the workspace’s mail, and `Billing` sees the plan and the invoices and nothing else. Both sit strictly inside `Admin`. They are seeded on the first read rather than at workspace creation, so a workspace made before this feature existed grows them the moment anything asks. `builtin` names which seed a row came from, and that is the whole of what it names: the six are a starting point a workspace is meant to shape, and every one of them but `Owner` can be renamed, repermissioned and deleted. Branch on `editable` and `deletable` rather than on the name: a role somebody renamed still answers those two correctly, and its name no longer tells you anything.

Owner is the only exception, and it is an exception in every direction: `editable: false`, `deletable: false`, and refused as a target on `PATCH /members/{userId}`. It describes the account the workspace is keyed on and holds every permission, including ones added in a later release, which is why its list is computed rather than stored. Making somebody else the owner is a workspace transfer; there is no endpoint here that performs one.

The other five accept everything: a new permission list, a new description, a new name, a `DELETE`. They are seeded defaults rather than fixtures: a workspace that never builds an integration should be able to be rid of `Developer`, and one where “Member” means something narrower should be able to say so in its own words. Only the owner refuses, and it refuses the lot under one code: `role_immutable`, a 409 carrying `param: "roleId"`, whether the PATCH held a name or a permission list. No rename is refused on its own any more, so there is no `param: "name"` immutability to handle; the only 409 a name can still raise is `role_name_taken`, when another role on the workspace already answers to it.

Past the six, a workspace writes up to 24 roles of its own. The ceiling counts only those, so deleting a seeded role buys no room under it. Permissions are EXPANDED on the way in rather than taken literally (`templates:write` alone is stored as `templates:read` and `templates:write`), so read the list back off the response rather than assuming it is the one you sent.

> A role is also the ceiling on an API key. A key issued against one may do `key.scopes ∩ role.permissions` and no more, resolved per request at the boundary, so editing a role changes what its keys may do on their very next call, and a key with no role has no ceiling at all. `account:read` and `account:write` are the exception, which no role limits. The Scopes page has the whole of that.

## Example

Needs `roles:read`. A page at a time, seeded roles first: pass `limit`, 1 to 100 and 25 by default, and follow `nextCursor` while `hasMore` is true.

**curl**

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

**Response**

```
{
  "object": "list",
  "data": [
    {
      "object": "role",
      "id": "role_1c94e05d3862c1f0a44b7f3a",
      "name": "Owner",
      "description": "Holds every permission there is, including the ones added later.",
      "permissions": ["emails:send", "emails:read", "…", "workspace:manage"],
      "builtin": "owner",
      "editable": false,
      "deletable": false,
      "members": 0,
      "apiKeys": 2,
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-01T09:00:00.000Z"
    },
    {
      "object": "role",
      "id": "role_c40a95f21cc65d31c2a89e07",
      "name": "Viewer",
      "description": "Reads the mail on the addresses they hold, changing nothing.",
      "permissions": [
        "emails:read",
        "drafts:read",
        "threads:read",
        "files:read",
        "labels:read",
        "contacts:read",
        "audiences:read",
        "calendar:read",
        "templates:read",
        "rules:read",
        "connections:read",
        "settings:read",
        "forms:read",
        "knowledge:read",
        "automations:read"
      ],
      "builtin": "viewer",
      "editable": true,
      "deletable": true,
      "members": 3,
      "apiKeys": 1,
      "createdAt": "2026-08-01T09:00:00.000Z",
      "updatedAt": "2026-08-01T09:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

> Sorted by built-in rank then name (owner, admin, member, viewer, developer, billing, then the rest alphabetically) rather than newest-first like the rest of the API. A permission matrix is read as a ladder, and sorting it by `createdAt` puts the widest role in a different row every week.

> Reading this list is what SEEDS the six on a workspace that has never had any. The seeding conflicts on a unique index and does nothing the second time, so the call is idempotent and only the first one writes, which is also why `POST /members` can always name a `roleId` that exists.

> It seeds ONCE. The workspace records that it has been seeded, so this read fills in a workspace older than the feature and then never writes again, which is what makes deleting a seeded role permanent. An earlier build re-inserted whatever template row was missing on every read, so a deleted `Billing` came back under a new `id` on the next page load; it does not any more.

> `members` and `apiKeys` are what would have to be moved before the role could go, which is what lets a client warn before offering the delete rather than after the 409. The owner row usually reads `members: 0`: the owner is not a member of their own workspace, they are the account it is keyed on.

> A workspace holds 24 custom roles by default precisely so the whole matrix stays readable. A workspace with forty roles cannot answer “who can send as billing@” by looking, which is the only question the feature exists to make answerable.

## Reference

- [`GET /roles`](https://nextmail.sh/docs/api/reference/roles#get-roles): full reference
