Skip to the documentation
API

Scopes

What a key is allowed to do.

The vocabulary

A closed set, resource:action. Small enough to show a human on a checkbox list, and stable enough that a stored grant still means the same thing a year later. The same vocabulary is what a workspace ROLE is written in, what an app’s OAuth approval is made of, and what gates MCP tools, so a read-only client cannot even see a sending tool. One alphabet, four surfaces.

ScopeGrants
emails:sendSend email
emails:readRead sent messages and their delivery status
drafts:readRead drafts
drafts:writeCreate and edit drafts
threads:readRead threads and messages
threads:writeLabel, read and archive threads
files:readList, search and download every file, and read its links
files:writeUpload and delete files, and publish or revoke their links
labels:readRead labels
labels:writeCreate and edit labels
contacts:readRead contacts
contacts:writeAdd, edit and remove contacts, and record events that can start automations
audiences:readRead audiences and who is in them
audiences:writeCreate and edit audiences, and change who is in them
calendar:readRead calendar events and invitations
calendar:writeCreate, change and respond to calendar events
templates:readRead email templates and preview them
templates:writeCreate, edit and send with email templates
domains:readRead domains and their DNS status
domains:writeVerify and configure domains
webhooks:readRead webhook endpoints and deliveries
webhooks:writeCreate, edit and test webhooks
rules:readRead mail rules and test them
rules:writeCreate, edit and reorder mail rules
connections:readRead which mailboxes are connected
members:readSee who is in the workspace and what they hold
members:writeAdd and remove people, and change what they can reach
roles:readRead the roles this workspace defines
roles:writeCreate, edit and delete roles
settings:readRead mailbox settings, including the signature
settings:writeChange mailbox settings and the signature
keys:writeReplace its own secret without anyone opening the console
keys:readRead the API keys it covers, their request log and their activity
keys:manageCreate, change, rotate and revoke keys, never wider than itself
forms:readRead sign-up forms and the people who filled them in
forms:writeCreate, edit, publish and delete sign-up forms
knowledge:readRead and search the knowledge base the AI uses
knowledge:writeAdd, edit, pin and remove knowledge base items
automations:readRead automations, who is in them and how they perform
automations:writeCreate, edit, publish and pause automations, and enroll contacts
tasks:readRead assistant tasks, what each run did and what it asked to approve
tasks:writeCreate, edit, run and pause assistant tasks, and answer what they ask to approve
connectors:readSee the apps connected for the workspace and what the assistant did through them
connectors:useHave the assistant look things up and make changes in the apps connected for the workspace
connectors:writeConnect, pause and disconnect apps for the workspace, and set what the assistant may do in them
billing:readSee the plan and invoices
billing:writeChange the plan and payment details
account:readRead your own profile, notification settings, workspaces, connected apps, invitations, signed-in devices, passkeys and security log
account:writeChange your own profile and notification settings, manage your workspaces and connected apps, answer invitations, sign your devices out, and rename or remove your passkeys

A key created without a considered scope list gets emails:send and nothing else. The safe default for a credential is the narrowest thing that makes it useful.

A key is capped by the role behind it

A key can be issued against a ROLE, and a role is a ceiling rather than a second grant. What the key may actually do is its own scopes INTERSECTED with that role’s permissions (key.scopes ∩ role.permissions), computed once at the boundary, on every request, before any endpoint is reached. Nothing downstream knows roles exist: a scope the role does not hold is simply not in the list the scope checks read. The exceptions are account:read and account:write: they reach only the person the key acts for, so no role limits them.

So the two lists are read together and neither wins on its own. A key carrying emails:send under a role that does not hold it may not send; a role holding emails:send gives nothing to a key that never asked for it. Ticking a scope is asking for authority, and the role decides how much of what you asked for you get.

A key with NO role has no ceiling, and is therefore as wide as the workspace it was issued against. That is what every key created before roles existed carries and what an owner still gets by leaving the field alone, so a null role is the WIDEST state a key can be in, not the narrowest. It is also why deleting a role makes you say where its keys should go: orphaning them would quietly promote every one of them.

The intersection is resolved per request rather than stamped on the key at issue time. That makes narrowing a role a live revocation, in force on the caller’s next call without the key having to be rotated, and widening one live in exactly the same way, which is the half worth remembering.

GET /ping and GET /keys/self report scopes beside grantedScopes and roleId for one failure in particular. scopes is the effective list and the only one that authorises anything; grantedScopes is what the key was issued with. Anything in the second and missing from the first was taken by the role, and that difference is the whole answer to “my key has emails:send and I am getting insufficient_scope”. The fix is a role change rather than another key.

A key the role has narrowed
curl "$OE/ping" -H "$AUTH" {  "ok": true,  "keyId": "4c1b257a66287fd113bd89d0",  "mode": "live",  "scopes": ["emails:read", "threads:read"],  "roleId": "role_c40a95f21cc65d31c2a89e07",  "grantedScopes": ["emails:send", "emails:read", "threads:read"],  "workspaceId": "10417196-e324-4283-af98-66ec62167c47"}

Four permissions can never reach a key at all: addresses:all, api-keys:read, api-keys:write and workspace:manage. They are permissions but not scopes, so no role however generous can put them on a token. billing:read and billing:write are scopes, and billing answers a key or a token only for the workspace owner. addresses:all would add nothing to a key in any case: a key with no allowlists already reaches every address. Keys are managed over the API with three scopes of their own instead: keys:write lets a key replace its own secret and nothing else, keys:read reads the workspace’s keys and their logs, and keys:manage creates, changes, rotates and revokes keys, never wider than the key making the call. GET /roles/permissions marks the four scope: false, which is what lets one component render both the role matrix and the key-creation checkbox list.

roles:write is effectively the whole vocabulary, and pretending otherwise would be the more dangerous documentation. A key holding it can PATCH the very role that caps it and hand itself everything else, and because the ceiling is resolved per request the wider one applies on the very next call. That is not a hole to plug, since a role editor that cannot edit roles 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.

Send scope

Separately from scopes, a key can be narrowed in what it may send as. It carries two lists. domainAllowlist holds whole domains, and a key holding a domain may send as any address on it, including addresses created after the key was. addressAllowlist holds single addresses. Leave both empty and the key is as wide as the workspace, never wider. GET /keys/self shows both lists and GET /addresses reports what a given key may actually use, which is the answer to an unexplained from_address_forbidden.

The same set narrows what the key reads. Sent mail, tracking, calendar and mail rules answer only for addresses the key may send as: GET /emails applies that filter before it cuts a page, so every page but the last is full, and GET /rules lists only the rules that can act on mail delivered to those addresses. So a key scoped to one domain neither sends nor reads on behalf of another. A whole domain also lets the key set that domain’s tracking host, which a key limited to single addresses cannot do.

Contacts, audiences and broadcasts belong to the whole workspace, and a key limited to particular addresses or domains uses them like any other key: it lists, saves, edits and deletes contacts and their photos, manages every audience, and previews and sends broadcasts. Two things stay narrowed. A broadcast is sent from an address the key may send as, or it is a 403 from_address_forbidden, and the key lists, reads and cancels only the broadcasts sent from an address or domain it holds, so any other one is a 404 broadcast_not_found. GET /contacts/people gives it the saved contacts alone, with seen false, even when it holds threads:read.

Such a key is still refused with 422 capability_unsupported on GET /contacts/{email}/threads and GET /contacts/{email}/activity, which read the mail of every address, and on POST and DELETE /contacts/{email}/block, because the blocklist filters mail for the whole workspace. An OAuth app works the same way when the workspace owner connected it. An app connected by a member is refused on every contacts, audiences and broadcasts route, even when their role reaches every address, and they manage them in the app instead. In the app itself, a member other than the owner sees and manages only the contacts, audiences and broadcasts they added.

Three narrowings, then, and they compose rather than override: the scopes on the key, the permissions of the role above it, and the domains and addresses it may put in a From header. A send needs all three, and a refusal names only the first one it met.