Skip to the documentation
SDK

Roles

`roles.list`, `get`, `create`, `update`, `delete` and `listPermissions`.

Every method

roles.ts
const roles = await openemail.roles.list()const role = await openemail.roles.get('role_…') const support = await openemail.roles.create({  name: 'Support',  description: 'Answers the shared inboxes and nothing else.',  permissions: ['emails:send', 'threads:write', 'labels:write'],}) console.log(support.permissions) await openemail.roles.update(support.id, {  permissions: [...support.permissions, 'templates:read'],}) await openemail.roles.delete(support.id, { reassignTo: 'role_…' }) const vocabulary = await openemail.roles.listPermissions()

support.permissions holds six entries, not three: emails:send brings emails:read, threads:write brings threads:read and labels:write brings labels:read. Read the list back rather than assuming it.

A role says what somebody may DO. Which ADDRESSES they may do it to is the other axis and lives on openemail.members. See grantAddress and revokeAddress there. “May send mail” and “may send as invoices@” are different sentences, and a workspace that hires a second support agent changes the second without touching the first.

Branch on editable and deletable rather than on builtin’s name. Both are false for the owner alone, whose list is “every permission, including ones invented next year” and is computed rather than stored; every other role answers true to both, the five a workspace is seeded with included. A role somebody renamed still answers both correctly, and its name no longer tells you anything.

update REPLACES the permission list. There is no grant-one call, so read the role, change the entry you meant and send all of them back. Sending one permission leaves the role holding exactly that one, plus whatever it implies.

delete needs reassignTo the moment anybody holds the role, and it travels as a query parameter because a body on DELETE is dropped by several runtimes and a number of proxies. The result reports reassigned and keysReassigned separately, so a script can log what it did rather than what it asked for.

listPermissions() is GET /roles/permissions, a fixed path sitting exactly where a role id would go. The client hard-codes it rather than passing the string through get, so asking for a role genuinely called “permissions” asks for a role and gets a 404, which is the honest answer to what was typed. scope: false marks the entries no key can ever hold.

A role is the ceiling on a key

A key issued against a role may do its own scopes INTERSECTED with that role’s permissions, resolved per request at the boundary. So narrowing a role revokes its keys live, without any of them being rotated, and a key with no role has no ceiling at all, which makes a null role the widest state a key can be in, not the narrowest.

That is also why roles.delete insists on somewhere to move the keys to. Orphaning them would drop their ceiling entirely, quietly promoting every credential the role was capping.

GET /keys/self and GET /ping report roleId and grantedScopes beside the effective scopes, which is how “my key has emails:send and I am getting insufficient_scope” gets answered: anything in grantedScopes and missing from scopes was taken by the role. openemail.me.get() and openemail.me.ping() return both, typed.

Parameters

namestringrequired
What the workspace calls the role: 1 to 48 characters, trimmed before it is stored. Names are unique per workspace case-insensitively, so a second "Support" is refused with `role_name_taken` (409) rather than created alongside the first.
descriptionstring
A sentence saying what the role is for, trimmed and at most 240 characters. A string that is blank once trimmed is stored as null, so a description of spaces comes back as null rather than as what you sent.
permissionsPermission[]required
What the role grants, drawn from the vocabulary `listPermissions()` serves; a string that is not in it is a 422 on `permissions` rather than being quietly dropped, so a typo is reported instead of costing you an afternoon. The list is EXPANDED on the way in (`templates:write` stores `templates:read` beside it), deduplicated and put back into canonical order, so read the stored list off the response rather than assuming it is the one you sent.

Response

object'role'
Always `role`. The delete tombstone answers with the same value, the role's `id`, `deleted: true` and the two reassignment counts, and none of the other fields below.
idstring
The role's id. It is what a member's `roleId` names, what an API key's ceiling points at, and what `reassignTo` takes when this role is deleted.
namestring
The workspace's name for the role, trimmed and unique case-insensitively. Every role but the owner's can be renamed, the seeded ones included (`builtin` says where a row came from, not what it has to stay called), so do not read "Admin" as a promise about what the role holds. A name another role already answers to is `role_name_taken` (409, `param: "name"`); renaming the owner is `role_immutable` (409), like every other edit of it.
descriptionstring | null
The sentence describing the role, or null when none was given. Blank input is stored as null on both create and update, so this is never an empty string.
permissionsPermission[]
Everything the role grants, already expanded and in canonical order rather than in the order anybody typed. That ordering is load-bearing: two roles holding the same permissions compare equal as JSON, which is what lets a settings screen diff them to decide whether Save is enabled.
builtin'owner' | 'admin' | 'member' | 'viewer' | 'developer' | 'billing' | null
Which of the six seeded roles this row came from, or null for one the workspace wrote itself. It records the seed and not a status: a seeded role is renamed, repermissioned and deleted like any other. Branch on `editable` and `deletable` rather than on this. A role somebody called "Admin" need not be the seeded one, and the seeded one may no longer be called that.
editableboolean
Computed as `builtin !== 'owner'`, so it is false for the owner role alone and every PATCH of that role is refused with `role_immutable` (409). Every other role is editable in full (name, description and permissions), including the five a workspace is seeded with.
deletableboolean
Computed as `builtin !== 'owner'`: false for the owner role alone, which comes back `role_undeletable` (409), and true for every other role including the seeded ones. Check it before offering the button rather than after the refusal, though a role somebody still holds also needs `reassignTo`, or the delete is `role_in_use` (409).
membersnumber
How many people hold this role, counted from the workspace's member rows. The owner is not among them: they have no member row and cannot be given a role, so the Owner role reports zero holders even though the members list shows them.
apiKeysnumber
How many live API keys are capped by this role; revoked keys are left out of the count, though a delete re-points every key row pointing at the role, revoked ones included. It is the second population that has to be moved before the role can go, and the one nobody notices: keys are programs, and a program does not complain.
createdAtstring
When the role row was written, ISO-8601. Built-in rows are seeded lazily the first time something needs them, such as a roles list read, a role create or the API-key screen, rather than at workspace creation, so a built-in's timestamp is when that first request landed and not when the workspace was made.
updatedAtstring
When the role last changed, ISO-8601. Every accepted PATCH moves it, including one that sets a field to the value it already held.