Aller à la documentation
API

Rôles

Chaque opération de ce groupe : ce qu'elle accepte, ce qu'elle retourne et les erreurs qu'elle peut renvoyer.

Opérations

What somebody may DO in this workspace. A role is a list of permissions drawn from the same alphabet as the API scopes, and that shared alphabet is what makes the interesting question answerable at all: a key issued by a member who may not touch templates is a key that may not touch templates.

The rule to hold on to is the intersection. The authority of a key is its own scopes INTERSECTED with the permissions of the role it was issued under, never the union, and it is resolved on every request rather than frozen into the token, so narrowing a role takes effect on the next request made under it, with no key rotation needed to make it stick, and widening one takes effect just as fast. A key with no role has no ceiling, which is what every key issued before roles existed still has.

Six roles are seeded on every workspace: owner, admin, member, viewer, developer and billing. They arrive on the first read of GET /roles rather than at workspace creation, so a workspace older than the feature acquires them the moment anybody looks. Only the OWNER is fixed. It holds every permission, including the ones added next year, and refuses every edit, rename and deletion. The other five are a starting point and nothing more: rename them, rewrite what they grant, or delete the ones this workspace has no use for. builtin records which template a role was seeded from, which is what fixes the list order and what a deletion falls back to; it is not a claim about what may be done to it.

The other axis lives next door under Members: which ADDRESSES somebody may act on. Both have to agree before anything happens.

GET/roles

List roles

Portéesroles:readLit

Every role this workspace defines: the seeded ones first in ladder order (owner, admin, member, viewer, then developer and billing), and everything else alphabetically after them. Not newest-first like the rest of the API, because a permission matrix is read as a ladder and ordering it by createdAt puts the widest role in a different row every week. A renamed seed keeps its rung: the order is read off builtin, not off the name.

A page at a time: follow nextCursor while hasMore is true to read every role. Custom roles are capped on purpose: a workspace with forty of them cannot answer "who can send as billing@" by looking, which is the only question the feature exists to make answerable.

A workspace older than the feature has no role rows at all, and this read SEEDS them rather than reporting an empty list. The seeder writes the six templates and rewrites nothing else, so an edited or renamed role is left exactly as it was, and it is what makes POST /members able to name a roleId that exists. The one row it does rewrite is owner, whose permission list is reset from the current vocabulary on every read, so a workspace seeded two releases ago still holds the permissions added since.

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 the templates again. That is what a client should know before it draws a confirmation dialog: deleting a seeded role is permanent. A deleted Billing stays deleted and does not come back under a new id on the next read. owner is the exception, restored on every read because it is the row the workspace is keyed on, and it cannot be deleted through this API in any case.

What a KEY may do is its own scopes INTERSECTED with the permissions of the role it was issued under, never the union. A role here that lacks emails:send is a ceiling: a key carrying that scope under that role cannot send, whatever the token says it holds. A key with no role has no ceiling, which is what every key issued before roles existed still has.

Requires the roles:read scope.

Paramètres de requête

limitinteger

Rows per page, 1 to 100.

Au moins 1Au plus 100Par défaut25
cursorstring

The previous page's nextCursor, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 invalid_cursor.

Retourne

A page of roles, the seeded ones first.

Erreurs

Les erreurs que toute opération peut renvoyer400401403404422500Catalogue des erreurs

Aussi disponible dans

SDK
roles.list()roles.listAll()roles.iterate()
CLI
openemail roles list
MCP
listDomainslistRoles

POST/roles

Create a role

Portéesroles:writeModifie des données

Writes a role this workspace invents for itself. builtin comes back null, and only rows like that count against the ceiling. The seeded roles already exist and are not created here; they are not fixtures either, so shaping one of them into what this workspace calls people is often a PATCH rather than a new role.

Implied permissions are expanded as it is written, so a body naming templates:write alone comes back holding templates:read as well. Storing the expanded list is what lets every reader (this API, the console, the key middleware) check one flat array rather than each of them knowing the implication table.

Mind what roles:write is. A key holding it can PATCH the very role that caps it and hand itself the rest of the vocabulary on the next request, because the ceiling is resolved per request rather than frozen into the token. That is the same authority the console gives anybody who can edit roles, and it is not a hole to be plugged here. A role editor that cannot edit its own role is not a role editor. It is a reason not to put roles:write on a key that only ever needed to read the members list.

A permission the calling key or token does not hold is refused with insufficient_authority, a 403, and none of them holds a console-only permission (scope: false on GET /roles/permissions). A role holding addresses:all, billing, workspace:manage or the API-key pair is written in the app.

Requires the roles:write scope.

Corps de la requête

namestringObligatoire

Unique per workspace. Trimmed on the way in; a name of nothing but spaces is a 422.

De 1 à 48 caractères
descriptionstring

For whoever reads the role list later. One sentence about who it is for.

Jusqu'à 240 caractères
permissionsstring[]Obligatoire

The whole list, and only entries from the vocabulary. A string this API does not recognise is REFUSED rather than dropped: the normaliser behind this ignores what it cannot place, which is right where it also serves the seeding path, and wrong here. A caller who posts templates:writ, gets a 201 back and discovers the role cannot edit templates has been told nothing and will spend the afternoon on it.

Implied permissions are expanded on the way in, so what comes back may be longer than what was sent. templates:write alone stores as templates:read and templates:write, because a role that can edit a template it cannot open is not a policy anybody means.

Jusqu'à 44 élémentsL'un de"emails:send""emails:read""drafts:read""drafts:write""threads:read""threads:write""files:read""files:write""labels:read""labels:write""contacts:read""contacts:write""audiences:read""audiences:write""calendar:read""calendar:write""templates:read""templates:write""domains:read""domains:write""webhooks:read""webhooks:write""rules:read""rules:write""connections:read""members:read""members:write""roles:read""roles:write""settings:read""settings:write""keys:write""keys:read""keys:manage""forms:read""forms:write""billing:read""billing:write""account:read""account:write""addresses:all""api-keys:read""api-keys:write""workspace:manage"

Retourne

201Role

Created. Held by nobody yet, so both counts are zero.

Erreurs

409

role_name_taken. Names are unique per workspace.

422

invalid_parameter naming an unrecognised permission, or workspace_limit_reached when the workspace is at its limit of custom roles.

Les erreurs que toute opération peut renvoyer400401403404500Catalogue des erreurs

Aussi disponible dans

SDK
roles.create()
CLI
openemail roles create
MCP
createRole

GET/roles/permissions

Every permission, described

Portéesroles:readLit

The vocabulary a role is written in: every permission, the sentence to show a person for it, the heading it belongs under, and whether a key may hold it at all.

Served rather than left to be transcribed, for the same reason the rule vocabularies are derived rather than described: a matrix built from a copied array keeps offering a permission the day one is renamed, and never offers the one added last week.

scope is the field to branch on. Permissions and scopes are one alphabet on purpose (the authority of a key is its own scopes intersected with the permissions of its role, and an intersection is only computable if both sides are drawn from the same list), but they are not the same set. The six console-only entries (addresses:all, the API-key pair, billing, workspace:manage) exist so that a role can say who reaches every address, who may see or move the plan and who may manage the workspace, and no token can ever carry them. The API-key pair is listed too, but API keys stay with the workspace owner whatever a role holds. A key-creation checklist filters on scope; a role matrix does not.

In canonical order, which is the order a stored permissions array comes back in, so a client rendering this list and a client rendering a role show the same permissions in the same sequence.

Behind roles:read, where the console has the same catalogue ungated. The divergence is deliberate: a signed-in person has to be shown the matrix before they can be told which parts of it they may not touch, whereas every request here already carries a key, and a key that may not read roles has no use for the vocabulary of roles.

Requires the roles:read scope.

Retourne

The whole vocabulary, in canonical order.

Erreurs

Les erreurs que toute opération peut renvoyer400401403404422500Catalogue des erreurs

Aussi disponible dans

SDK
roles.listPermissions()
CLI
openemail roles list-permissions
MCP
listDomainslistPermissionslistRoles

GET/roles/{id}

Retrieve a role

Portéesroles:readLit

members and apiKeys are counted at read time rather than stored, so they are the answer now and not the answer when somebody last edited the role. They are what a deletion would have to move.

Requires the roles:read scope.

Paramètres de chemin

idstringObligatoire

A role_ id. Roles have no slug. The name is editable and therefore not a handle.

Retourne

200Role

The role, with its usage counts.

Erreurs

Les erreurs que toute opération peut renvoyer400401403404422500Catalogue des erreurs

Aussi disponible dans

SDK
roles.get()
CLI
openemail roles get
MCP
getRole

PATCH/roles/{id}

Update a role

Portéesroles:writeModifie des données
Demande un code de vérification

Rename a role, rewrite its sentence, or replace what it grants.

permissions is REPLACE-WHOLE. There is no way to add or remove a single one and there will not be: the list is what gets audited, and an index-addressed patch is a lost update the first time two tabs are open. "Add domains:write" is a GET and a PATCH in a client that already has the array on screen.

The owner role refuses every edit (changing it would be changing what "owner" means, which is not for a workspace to decide), and it is the only role that refuses one. Everything else takes all three fields, the seeded Admin, Member, Viewer, Developer and Billing included: renaming Billing to "Finance" and rewriting what it grants is an ordinary PATCH. builtin does not move with the name; it goes on recording which template the row was seeded from, which is what keeps the list order stable.

The edit is LIVE. Authority is resolved per request (the scopes on a key intersected with the permissions on this role, every time), so narrowing a role takes effect on the next request anybody holding it makes, with no key rotation needed to make it stick, and widening one takes effect just as immediately, which is the half worth remembering before handing somebody a role to edit.

Adding a permission the calling key or token does not hold is refused with insufficient_authority, a 403, and none of them holds a console-only one such as addresses:all. One the role already holds is kept only if the PATCH sends it back, and a console-only one cannot be added back through this API, so send back every entry you read.

Requires the roles:write scope.

Paramètres de chemin

idstringObligatoire

A role_ id. Roles have no slug. The name is editable and therefore not a handle.

Corps de la requête

namestring

Unique per workspace. Trimmed on the way in; a name of nothing but spaces is a 422.

De 1 à 48 caractères
descriptionstring

Nullable as well as optional, and the difference is the whole point of a patch: omit it to keep the stored sentence, send null to clear it. Without the null there would be no way to remove a description once written except by replacing it with a space.

Peut être nullJusqu'à 240 caractères
permissionsstring[]

The whole list, and only entries from the vocabulary. A string this API does not recognise is REFUSED rather than dropped: the normaliser behind this ignores what it cannot place, which is right where it also serves the seeding path, and wrong here. A caller who posts templates:writ, gets a 201 back and discovers the role cannot edit templates has been told nothing and will spend the afternoon on it.

Implied permissions are expanded on the way in, so what comes back may be longer than what was sent. templates:write alone stores as templates:read and templates:write, because a role that can edit a template it cannot open is not a policy anybody means.

Jusqu'à 44 élémentsL'un de"emails:send""emails:read""drafts:read""drafts:write""threads:read""threads:write""files:read""files:write""labels:read""labels:write""contacts:read""contacts:write""audiences:read""audiences:write""calendar:read""calendar:write""templates:read""templates:write""domains:read""domains:write""webhooks:read""webhooks:write""rules:read""rules:write""connections:read""members:read""members:write""roles:read""roles:write""settings:read""settings:write""keys:write""keys:read""keys:manage""forms:read""forms:write""billing:read""billing:write""account:read""account:write""addresses:all""api-keys:read""api-keys:write""workspace:manage"

Retourne

200Role

Saved. In force on the next request anybody holding it makes.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

409

role_name_taken with param: "name" (names are unique per workspace, case-insensitively), or role_immutable with param: "roleId", which is the owner role and nothing else.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
roles.update()
CLI
openemail roles update
MCP
updateRole

DELETE/roles/{id}

Delete a role

Portéesroles:writeSupprime
Demande un code de vérification

Deletes a role and moves everybody who held it. Only the owner refuses: the roles a workspace was seeded with go the same way as one somebody wrote, because a workspace that never uses Developer or Billing should be able to be rid of them.

Deleting a seeded role is permanent, the same as deleting one somebody wrote. Seeding is a one-time bootstrap recorded against the workspace, not a reconciliation run on every read, so nothing puts a deleted role back.

API keys are re-pointed rather than orphaned, and that is the part worth reading twice: a key whose role had vanished would fall back to NO ceiling, and no ceiling is WIDER than a narrow role, so deleting a restrictive role would quietly promote every key it had been capping. Refusing without reassignTo is what keeps that from being a one-word request.

The two counts come back separately because they are two different things to go and check afterwards. reassigned is people; keysReassigned is programs, which will carry on running under their new ceiling without anybody being told.

A tombstone rather than a 204, matching the rest of the API: the id comes back so a log line can name what went.

Requires the roles:write scope.

Paramètres de chemin

idstringObligatoire

A role_ id. Roles have no slug. The name is editable and therefore not a handle.

Paramètres de requête

reassignTostring

A role id to move the holders to. Required the moment the role is actually held; a role held by nobody deletes without it, and the service refuses to guess a destination in either case.

A query parameter rather than a body, because a DELETE body is dropped by several fetch implementations and by a fair number of proxies. A caller who sent one would be told the role is still in use with no way to see that their body never arrived.

Retourne

200object

Deleted, with what had to be moved.

objectstring
L'un de"role"
idstring
deletedboolean
L'un detrue
reassignedinteger

People moved to reassignTo. They will notice.

keysReassignedinteger

API keys re-pointed at reassignTo. They will not.

Erreurs

403

The key lacks the scope, or may not send as that address.

step_up_required: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with POST /security/step-up, send it to POST /security/step-up/verify, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.

409

role_in_use with param: "reassignTo" (name somewhere to move the holders to), or role_undeletable, which is the owner role and nothing else. role_immutable on reassignTo is the third: the owner role cannot be the destination either.

Les erreurs que toute opération peut renvoyer400401404422500Catalogue des erreurs

Aussi disponible dans

SDK
roles.delete()
CLI
openemail roles delete
MCP
deleteRole

Objets

PermissionDescriptorobject

objectstring
L'un de"permission"
idstring

One permission from the closed vocabulary. Every API scope is a permission; the reverse does not hold. See scope on PermissionDescriptor.

L'un de"emails:send""emails:read""drafts:read""drafts:write""threads:read""threads:write""files:read""files:write""labels:read""labels:write""contacts:read""contacts:write""audiences:read""audiences:write""calendar:read""calendar:write""templates:read""templates:write""domains:read""domains:write""webhooks:read""webhooks:write""rules:read""rules:write""connections:read""members:read""members:write""roles:read""roles:write""settings:read""settings:write""keys:write""keys:read""keys:manage""forms:read""forms:write""billing:read""billing:write""account:read""account:write""addresses:all""api-keys:read""api-keys:write""workspace:manage"
labelstring

What to put beside the checkbox. The same sentence the scope list on a key shows, from the same table, so nobody is told two different things about one word.

groupstring

The heading it renders under. other is the fallback for a permission that has been added to the vocabulary and not yet placed in a group, reported rather than omitted, because a permission nobody can see in the matrix is a permission nobody audits.

L'un de"addresses""mail""organising""writing""workspace""people""developer""other"
scopeboolean

Whether an API KEY can hold this. False for the six console-only permissions (addresses:all, the API-key pair, billing and workspace:manage), which only a role holds and no token can ever carry: they say who reaches every address, who may see or change the plan and who may manage the workspace. The API-key pair is listed too, but API keys stay with the workspace owner whatever a role holds. Filter a key-creation checklist on this rather than on a list of your own.

Roleobject

objectstring
L'un de"role"
idstring

The durable handle, role_ + 24 hex.

namestring

Unique per workspace and compared case-insensitively, so "support" beside "Support" is a 409 rather than a silent duplicate. Editable on every role but the owner, which makes a name a label and never a handle: builtin is what says which template a row was seeded from, and id is what names it again tomorrow.

descriptionstring
Peut être null
permissionsstring[]

What somebody holding this role may DO. Which addresses they may do it to is the other axis, and it lives on Member.addresses, with one exception: addresses:all reaches every address on every domain of the workspace, including ones added later, with no grant at all.

L'un de"emails:send""emails:read""drafts:read""drafts:write""threads:read""threads:write""files:read""files:write""labels:read""labels:write""contacts:read""contacts:write""audiences:read""audiences:write""calendar:read""calendar:write""templates:read""templates:write""domains:read""domains:write""webhooks:read""webhooks:write""rules:read""rules:write""connections:read""members:read""members:write""roles:read""roles:write""settings:read""settings:write""keys:write""keys:read""keys:manage""forms:read""forms:write""billing:read""billing:write""account:read""account:write""addresses:all""api-keys:read""api-keys:write""workspace:manage"
builtinstring

Which template this row was seeded from, or null for a role somebody wrote. The seeds arrive on the first read of GET /roles, so a workspace older than the feature still has them. It is provenance and sort order, NOT protection: every value but owner can be renamed, rewritten and deleted like any other role, and a renamed one keeps this field.

Peut être nullL'un de"owner""admin""member""viewer""developer""billing"
editableboolean

False only for owner. Everything else takes a new name, a new description and a new permission list, the seeded roles included.

deletableboolean

True for every role but the owner. It also answers "may this be renamed", which is why there is no second flag saying so. It equals editable in every case today; the two are still reported apart because they name two different refusals, and a client should read the one it is about rather than learn that they happen to coincide.

membersinteger

People holding it. The owner is never counted.

apiKeysinteger

Keys capped by it. The quiet half of a deletion: people notice losing a role and programs do not.

createdAtstring
Formatdate-time
updatedAtstring
Formatdate-time

RoleListobject

objectstring
L'un de"list"
dataRole[]
hasMoreboolean

True when another page follows. Pass nextCursor back as cursor to read it.

nextCursorstring

An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

Peut être null