Create a role
A name and a list of permissions. What comes back is longer than what you sent.
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 -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"] }'{ "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.