Skip to the documentation
API

Create a role

A name and a list of permissions. What comes back is longer than what you sent.

POSTapi.openemail.uk/roles

Runs the real call against your workspace, with your own key.

POST /roles

A name and a list of permissions. What comes back is longer than what you sent.

Example

Needs roles:write. Returns 201. A custom role is builtin: null, editable: true and deletable: true, and is held by nobody until somebody is moved onto it.

curl
curl -X POST "$OE/roles" -H "$AUTH" -H "Content-Type: application/json" \  -d '{    "name": "Support",    "description": "Answers the shared inboxes and nothing else.",    "permissions": ["emails:send", "threads:write", "labels:write", "contacts:read"]  }'
Response
{  "object": "role",  "id": "role_2b81de079c1f0a4b7e05d386",  "name": "Support",  "description": "Answers the shared inboxes and nothing else.",  "permissions": [    "emails:send",    "emails:read",    "threads:read",    "threads:write",    "labels:read",    "labels:write",    "contacts:read"  ],  "builtin": null,  "editable": true,  "deletable": true,  "members": 0,  "apiKeys": 0,  "createdAt": "2026-08-30T10:41:02.000Z",  "updatedAt": "2026-08-30T10:41:02.000Z"}

Four permissions went in and seven came back. emails:send implies emails:read, threads:write implies threads:read, labels:write implies labels:read. A role that may archive a thread it cannot open is a checkbox somebody forgot rather than a policy anybody means, so the implication is stored rather than refused. The list also comes back in canonical order, which is what lets a client diff two roles as JSON and decide whether Save is enabled.

An unknown permission IS refused here rather than dropped: templates:writ is invalid_parameter, a 422, naming the string. The service normalises silently because it is also the seeding path and the MCP path, where failing a whole role over one unrecognised word would be worse. It is wrong on a call somebody made deliberately. A 200 carrying a role that cannot edit templates has told them nothing, and they will spend the afternoon on it.

A duplicate name on the same workspace is role_name_taken, a 409. The 25th custom role is role_limit_reached, a 422. It guards how big a matrix can get before nobody audits it, and is not a plan boundary.

Creating a role gives it to nobody. Moving people onto it is PATCH /members/{userId}, and pointing a key at it is done where the key is issued.