---
title: "openemail.roles"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://openemail.uk/docs/python/reference/roles"
area: "Python"
category: "Reference"
---

# openemail.roles

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

What a member or an API key may do, drawn from one permission vocabulary.

### `roles.list()`

List every role in the workspace

```python
def list(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[RoleResource]
```

Returns one page of the roles the workspace defines. `list_all` collects every page and `iterate` walks them lazily. The seeded roles come first in ladder order (Owner, Admin, Member, Viewer, Developer, Billing) and custom roles follow alphabetically. The order is read from `builtin`, so a renamed seeded role keeps its place.

A workspace older than roles has no role rows, and the first read seeds them instead of returning an empty list. Seeding runs once per workspace, and afterwards only the owner role is ever restored, so a seeded role you delete stays deleted. Each row carries `members` and `apiKeys` counts computed at read time.

A role is a ceiling for the keys issued under it. What a key may do is its own scopes intersected with its role's permissions, resolved on every request, so a key holding `emails:send` under a role without it cannot send. A key with no role has no ceiling.

Scopes: `roles:read`.

**Parameters**

- `limit` (`int`): Page size, from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` of the previous page. Leave it out for the first page.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[RoleResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each item has `id`, `name`, `description`, `permissions`, `builtin`, `editable`, `deletable`, `members`, `apiKeys`, `createdAt` and `updatedAt`.

**Example**

```python
from openemail import openemail

page = openemail.roles.list(limit=50)

for role in page['items']:
    kind = role['builtin'] or 'custom'
    print(role['name'], kind, role['members'], 'members', role['apiKeys'], 'keys')

if page['hasMore']:
    print('next page starts after', page['nextCursor'])
```

**Notes**

- Branch on `editable` and `deletable` rather than on `builtin`. Both are `False` only for the owner role.
- `members` counts people with an explicit membership, so implied members from `members.list` are not counted. `apiKeys` counts unrevoked keys only.
- A workspace holds at most 24 custom roles. Seeded roles do not count toward that.
- The cursor is opaque and holds where the last row sat in this order, so a row deleted or edited between pages never breaks the walk: the next page starts at the first row that sorts after it. A cursor this list did not hand out is a 400 `invalid_cursor`.

Also available in: API [`GET /roles`](https://openemail.uk/docs/api/reference/roles#get-roles); TypeScript [`roles.list()`](https://openemail.uk/docs/sdk/reference/roles#list); Ruby [`roles.list`](https://openemail.uk/docs/ruby/reference/roles#list); CLI [`openemail roles list`](https://openemail.uk/docs/cli/reference/roles#roles-list).

### `roles.list_all()`

Collect every role into one list

```python
def list_all(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[RoleResource]
```

Walks every page of `list` and returns all roles as one list, seeded roles first in ladder order, then custom roles alphabetically. One request per page.

Scopes: `roles:read`.

**Parameters**

- `limit` (`int`): Page size for each request, from 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk after this cursor instead of the first page.
- `api_key` (`str`): Overrides the client's API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[RoleResource]` holding every role.

**Example**

```python
from openemail import openemail

roles = openemail.roles.list_all(limit=100)

custom = [role['name'] for role in roles if role['builtin'] is None]

print(f'{len(roles)} roles, {len(custom)} custom:', ', '.join(custom))
```

**Notes**

- If any page fails, the call raises and the roles already fetched are discarded.

Also available in: API [`GET /roles`](https://openemail.uk/docs/api/reference/roles#get-roles); TypeScript [`roles.listAll()`](https://openemail.uk/docs/sdk/reference/roles#listAll); Ruby [`roles.list_all`](https://openemail.uk/docs/ruby/reference/roles#listAll).

### `roles.iterate()`

Stream the roles one at a time

```python
def iterate(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[RoleResource]
```

Returns a generator that yields roles one at a time, seeded roles first in ladder order, then custom roles alphabetically, and requests the next page only once the current one is used up. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.

Scopes: `roles:read`.

**Parameters**

- `limit` (`int`): Page size for each request, from 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk after this cursor instead of the first page.
- `api_key` (`str`): Overrides the client's API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[RoleResource]`, a generator yielding one role per step.

**Example**

```python
from openemail import openemail

for role in openemail.roles.iterate():
    if 'emails:send' in role['permissions']:
        print(role['name'], 'can send mail')
```

**Notes**

- The generator is lazy, so an abandoned loop costs only the pages it read.

Also available in: API [`GET /roles`](https://openemail.uk/docs/api/reference/roles#get-roles); TypeScript [`roles.iterate()`](https://openemail.uk/docs/sdk/reference/roles#iterate); Ruby [`roles.iterate`](https://openemail.uk/docs/ruby/reference/roles#iterate).

### `roles.get()`

Read one role with its usage counts

```python
def get(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RoleResource
```

Returns a single role by id. There is no lookup by name, because every role's name except the owner's can be edited.

`members` and `apiKeys` are counted when you call rather than stored, so they describe what a deletion would have to move right now. `permissions` is the stored list with implied permissions already expanded, in canonical order, so it can be longer than what was sent when the role was written.

Scopes: `roles:read`.

**Parameters**

- `id` (`str`, required): Role id, `role_` followed by 24 hex characters. Roles have no lookup by name.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RoleResource` with `permissions`, `builtin`, the `editable` and `deletable` flags, and live `members` and `apiKeys` counts.

**Example**

```python
from openemail import OpenEmailApiError, openemail

try:
    role = openemail.roles.get('role_8b1f4c2e9a7d3b60e5f1a2c4')
except OpenEmailApiError as error:
    if error.is_not_found:
        print('No role with that id in this workspace')
    else:
        raise
else:
    print(role['name'], role['permissions'])
    print(role['members'], 'members and', role['apiKeys'], 'keys hold it')
```

**Notes**

- An unknown id, or one from another workspace, is 404 `role_not_found` with `param` set to `roleId`, which raises `OpenEmailApiError` with `is_not_found` set. The two answer the same way on purpose, so a 404 does not tell you whether the role exists somewhere else.
- `builtin` records which template a row was seeded from and survives a rename. It is provenance and sort order, not protection.

Also available in: API [`GET /roles/{id}`](https://openemail.uk/docs/api/reference/roles#get-roles-id); TypeScript [`roles.get()`](https://openemail.uk/docs/sdk/reference/roles#get); Ruby [`roles.get`](https://openemail.uk/docs/ruby/reference/roles#get); CLI [`openemail roles get`](https://openemail.uk/docs/cli/reference/roles#roles-get).

### `roles.create()`

Create a custom role

```python
def create(
    body: RoleCreate,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RoleResource
```

Writes a role the workspace defines for itself. `builtin` comes back `None` and both usage counts are zero. Only roles like this count toward the ceiling of 24 custom roles, past which the call is 422 `role_limit_reached`.

Implied permissions are expanded as the role is stored, so `templates:write` alone comes back holding `templates:read` too, and `roles:write` brings `roles:read` and `members:read`. Read the final list off the result rather than the request. A string that is not in the vocabulary is refused with 422 `invalid_parameter` rather than dropped.

Be careful with `roles:write`. Authority is resolved per request, so a key holding it can edit the very role that caps it and widen itself on the next call. Keep it off keys that only need to read.

Scopes: `roles:write`.

**Parameters**

- `body['name']` (`str`, required): At most 48 characters after trimming, unique per workspace ignoring case. Blank is a 422.
- `body['permissions']` (`list[Permission]`, required): What the role grants, a list drawn from `list_permissions`. An empty list is accepted and makes a role that can do nothing.
- `body['description']` (`str`): One sentence about who the role is for, at most 240 characters. Blank is stored as `None`.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RoleResource` with the expanded `permissions`, `builtin` set to `None`, `editable` and `deletable` set to `True`, and `members` and `apiKeys` at 0.

**Example**

```python
from openemail import openemail

support = openemail.roles.create(
    {
        'name': 'Support',
        'description': 'Answers help@ and nothing else.',
        'permissions': ['threads:write', 'emails:send', 'templates:read'],
    }
)

print(support['id'], support['permissions'])
```

**Notes**

- A name that matches an existing role ignoring case, seeded roles included, is 409 `role_name_taken` with `param` set to `name`.
- A permission the calling key or access token does not hold is refused with 403 `insufficient_authority`. None of them can hold a console-only permission, the ones `list_permissions` marks with `scope` set to `False`, so a role with `addresses:all`, `api-keys:write` or `workspace:manage` in it is written in the app, never through the API.
- Not retried automatically. After a network failure the role may already exist, and creating it again is `role_name_taken`.

Also available in: API [`POST /roles`](https://openemail.uk/docs/api/reference/roles#post-roles); TypeScript [`roles.create()`](https://openemail.uk/docs/sdk/reference/roles#create); Ruby [`roles.create`](https://openemail.uk/docs/ruby/reference/roles#create); CLI [`openemail roles create`](https://openemail.uk/docs/cli/reference/roles#roles-create).

### `roles.update()`

Rename a role or replace what it grants

```python
def update(
    id: str,
    patch: RolePatch,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RoleResource
```

Patches the name, description or permission list of a role. Every seeded role except Owner takes all three, so renaming Billing to Finance and rewriting what it grants is an ordinary update. The owner role refuses any edit with 409 `role_immutable`.

`permissions` replaces the whole list and is expanded with implied permissions on the way in. There is no way to add or remove one entry, so read the role, change the list and send all of it. Leave a field out to keep it, and send `'description': None` to clear the note.

The change is live. Authority is resolved on every request, so narrowing a role takes effect for its members and keys on their next call without rotating anything, and widening it takes effect just as fast.

Scopes: `roles:write`.

**Parameters**

- `id` (`str`, required): Role id, `role_` followed by 24 hex characters. Roles have no lookup by name.
- `patch['name']` (`str`): New name, at most 48 characters after trimming and unique per workspace ignoring case. Blank is a 422.
- `patch['description']` (`str | None`): New note of at most 240 characters. `None` or blank clears it.
- `patch['permissions']` (`list[Permission]`): Complete replacement list, expanded with implied permissions before it is stored.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RoleResource` as saved, with live `members` and `apiKeys` counts.

**Example**

```python
from openemail import openemail

role = openemail.roles.get('role_8b1f4c2e9a7d3b60e5f1a2c4')

updated = openemail.roles.update(
    role['id'],
    {'name': 'Finance', 'permissions': [*role['permissions'], 'domains:read']},
)

print(updated['name'], updated['permissions'])
```

**Notes**

- A new name that clashes with another role ignoring case is 409 `role_name_taken`. `builtin` does not change with the name, which keeps a renamed seeded role in its place in `list`.
- An empty patch is accepted and only moves `updatedAt`.
- Adding a permission the calling key or access token does not hold is 403 `insufficient_authority`, and none of them holds a console-only one such as `addresses:all`. One the role already holds stays only if you send it back, and a console-only one cannot be added back through the API, so send back every entry you read.
- With an OAuth access token it asks for a verification code: until the app has verified one, it is refused with 403 `step_up_required`, and `is_step_up_required` on the error says so. An API key is never asked.
- Retried automatically on network failure and retryable statuses, since the same patch lands on the same row.

Also available in: API [`PATCH /roles/{id}`](https://openemail.uk/docs/api/reference/roles#patch-roles-id); TypeScript [`roles.update()`](https://openemail.uk/docs/sdk/reference/roles#update); Ruby [`roles.update`](https://openemail.uk/docs/ruby/reference/roles#update); CLI [`openemail roles update`](https://openemail.uk/docs/cli/reference/roles#roles-update).

### `roles.delete()`

Delete a role and move whoever holds it

```python
def delete(
    id: str,
    *,
    reassign_to: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> DeletedRoleResource
```

Deletes a role. Every role except Owner can go, seeded ones included, and deleting a seeded role is permanent because seeding never runs twice. The owner role is refused with 409 `role_undeletable`.

While any member, API key or unanswered invitation still points at the role, the call needs `reassign_to=` and is refused with 409 `role_in_use` without it. The server will not guess, because a key whose role vanished would fall back to no ceiling at all, which is wider than the role being removed. Members, keys and pending invitations move to the named role in one transaction. A role nobody holds deletes without it.

The tombstone reports `reassigned` people and `keysReassigned` keys separately. Log the second: those programs keep running under a new ceiling and nobody is told.

Scopes: `roles:write`.

**Parameters**

- `id` (`str`, required): Role id, `role_` followed by 24 hex characters. Roles have no lookup by name.
- `reassign_to` (`str`): Id of the role that inherits the members, keys and pending invitations. Required whenever the role is held. Sent as a query parameter.
- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`DeletedRoleResource`, a dict with `object` set to `role`, the `id`, `deleted` set to `True`, `reassigned` and `keysReassigned`.

**Example**

```python
from openemail import openemail

roles = openemail.roles.list_all()
viewer = next((role for role in roles if role['builtin'] == 'viewer'), None)

gone = openemail.roles.delete(
    'role_8b1f4c2e9a7d3b60e5f1a2c4', reassign_to=viewer['id'] if viewer else None
)

print(gone['reassigned'], 'members and', gone['keysReassigned'], 'keys moved')
```

**Notes**

- Naming the owner role in `reassign_to=` is 409 `role_immutable`, naming the role being deleted is 409 `role_in_use`, and an id that names no role on this workspace is 404 `role_not_found` with `param` set to `roleId`, whether it was the id being deleted or the one in `reassign_to=`.
- Revoked API keys still point at their role. A role whose `apiKeys` count is 0 can therefore still need `reassign_to=`, and those keys are included in `keysReassigned`.
- Pending invitations are moved as well but are not counted in the response.
- With an OAuth access token it asks for a verification code: until the app has verified one, it is refused with 403 `step_up_required`, and `is_step_up_required` on the error says so. An API key is never asked.
- Not retried automatically. A retry after a lost response is a 404, since the role is already gone.

Also available in: API [`DELETE /roles/{id}`](https://openemail.uk/docs/api/reference/roles#delete-roles-id); TypeScript [`roles.delete()`](https://openemail.uk/docs/sdk/reference/roles#delete); Ruby [`roles.delete`](https://openemail.uk/docs/ruby/reference/roles#delete); CLI [`openemail roles delete`](https://openemail.uk/docs/cli/reference/roles#roles-delete).

### `roles.list_permissions()`

List the permission vocabulary roles are written in

```python
def list_permissions(
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[PermissionResource]
```

Returns every permission as one list, in the canonical order a stored role's `permissions` also uses. Each entry carries the `label` to show beside a checkbox and the `group` heading it belongs under, so a permission matrix can be rendered from this rather than from a list copied into your code.

`scope` is the field to branch on. Permissions and API key scopes share one alphabet, which is what lets a role cap a key, but they are not the same set. Four are console-only. `addresses:all` lets a role reach every address on every domain of the workspace, including ones added later, with no grant, and `workspace:manage` says who may manage the workspace. `api-keys:read` and `api-keys:write` are listed too, but API keys stay with the workspace owner whatever a role holds. No key or access token can ever hold any of the four, so they come back with `scope` set to `False`. `billing:read` and `billing:write`, which say who may see or change the plan, are key scopes as well, so they come back with `scope` set to `True`. Filter on `scope` for a key scope picker and ignore it for a role editor.

Scopes: `roles:read`.

**Parameters**

- `api_key` (`str`): Overrides the client's API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[PermissionResource]`, each with `id`, `label`, `group` and `scope`.

**Example**

```python
from openemail import openemail

permissions = openemail.roles.list_permissions()

grantable_to_keys = [permission['id'] for permission in permissions if permission['scope']]

print(grantable_to_keys)
```

**Notes**

- `group` is one of `addresses`, `mail`, `organising`, `writing`, `workspace`, `people` or `developer`, with `other` as the fallback for a permission not yet placed in a group.
- The list is the same for every workspace, yet the call still requires `roles:read`.

Also available in: API [`GET /roles/permissions`](https://openemail.uk/docs/api/reference/roles#get-roles-permissions); TypeScript [`roles.listPermissions()`](https://openemail.uk/docs/sdk/reference/roles#listPermissions); Ruby [`roles.list_permissions`](https://openemail.uk/docs/ruby/reference/roles#listPermissions); CLI [`openemail roles list-permissions`](https://openemail.uk/docs/cli/reference/roles#roles-list-permissions).
