Roles
`roles.list`, `list_all`, `iterate`, `get`, `create`, `update`, `delete` and `list_permissions`.
Every method
page = client.roles.listputs page.items.size role = client.roles.get("role_8b1f4c2e9a7d3b60e5f1a2c4")puts role[:name] support = client.roles.create( name: "Support", description: "Answers the shared inboxes and nothing else.", permissions: ["emails:send", "threads:write", "labels:write"]) p support[:permissions] client.roles.update(support[:id], permissions: [*support[:permissions], "templates:read"]) client.roles.delete(support[:id], reassign_to: role[:id]) vocabulary = client.roles.list_permissionsp vocabulary.map { |permission| permission[:id] }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.
list returns one OpenEmail::Page, list_all returns every role in one Array, and iterate yields each role to a block or returns an Enumerator without one. A role comes back as a Hash with Symbol keys, so role[:permissions] reads the list. create and update take the body fields as keywords or as one Hash, while delete takes reassign_to:, a snake_case keyword the gem renames for the API.
A role says what somebody may DO. Which ADDRESSES they may do it to is the other axis and lives on client.members: see grant_address and revoke_address on the Members page. “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. One permission answers both: a role holding addresses:all reaches every address, including ones added later, with no grant, and only a person in the app can put it on a role.
Branch on editable and deletable rather than on builtin or on the 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, as [*support[:permissions], "templates:read"] does above. Sending one permission leaves the role holding exactly that one, plus whatever it implies.
delete needs reassign_to: the moment anybody holds the role. The gem sends it as the reassignTo query parameter, because a body on DELETE is dropped by several runtimes and a number of proxies, and leaves the parameter out when you pass nothing. The result reports reassigned and keysReassigned separately, so a script can log what it did rather than what it asked for.
list_permissions is GET /roles/permissions, a fixed path sitting exactly where a role id would go. The gem calls that path directly rather than passing the word through get, and returns a plain Array, not an OpenEmail::Page: one Hash per permission, with id, label, group and scope. scope: false marks the entries no key can ever hold. Do not pass the word to get yourself. client.roles.get("permissions") builds the same path, so it sends the same request and gets the vocabulary back rather than a role or a 404.
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. A key with no role has no ceiling at all, which makes a nil roleId 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. That 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. client.me.get and client.me.ping return both in their Hash, so key[:grantedScopes] - key[:scopes] lists what the role took. The refusal itself is an OpenEmail::PermissionError whose scope_missing? is true.
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), raised as `OpenEmail::ConflictError`, 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 nil, so a description of spaces comes back as nil rather than as what you sent. On `create`, leave it out rather than passing nil: the gem sends a nil as it is, and `create` refuses it with a 422. On `update`, `description: nil` clears it.
permissionsArray<String>required- What the role grants, drawn from the vocabulary `list_permissions` serves. A string that is not in it is a 422 on `permissions`, raised as `OpenEmail::ValidationError` with `param` set to `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
objectString- 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, read as `role[:id]`. It is what a member’s `roleId` names, what an API key’s ceiling points at, and what `reassign_to:` takes when another role is deleted and its holders move to this one.
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, with `param` set to `name`). Renaming the owner is `role_immutable` (409), like every other edit of it.
descriptionString or nil- The sentence describing the role, or nil when none was given. Blank input is stored as nil on both create and update, so this is never an empty string.
permissionsArray<String>- 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 hold equal Arrays, which is what lets a settings screen compare them with `==` to decide whether Save is enabled.
builtinString or nil- Which of the six seeded roles this row came from, `owner`, `admin`, `member`, `viewer`, `developer` or `billing`, or nil 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 `update` 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. A role somebody still holds also needs `reassign_to:`, or the delete is `role_in_use` (409). Both refusals are raised as `OpenEmail::ConflictError`, and `code` tells them apart.
membersInteger- 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.
apiKeysInteger- 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, as an ISO 8601 String. 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, as an ISO 8601 String. Every accepted `update` moves it, including one that sets a field to the value it already held.