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.
| Scope | Grants |
|---|---|
| emails:send | Send email |
| emails:read | Read sent messages and their delivery status |
| drafts:read | Read drafts |
| drafts:write | Create and edit drafts |
| threads:read | Read threads and messages |
| threads:write | Label, read and archive threads |
| files:read | List, search and download every file, and read its links |
| files:write | Upload and delete files, and publish or revoke their links |
| labels:read | Read labels |
| labels:write | Create and edit labels |
| contacts:read | Read contacts |
| contacts:write | Add, edit and remove contacts, and record events that can start automations |
| audiences:read | Read audiences and who is in them |
| audiences:write | Create and edit audiences, and change who is in them |
| calendar:read | Read calendar events and invitations |
| calendar:write | Create, change and respond to calendar events |
| templates:read | Read email templates and preview them |
| templates:write | Create, edit and send with email templates |
| domains:read | Read domains and their DNS status |
| domains:write | Verify and configure domains |
| webhooks:read | Read webhook endpoints and deliveries |
| webhooks:write | Create, edit and test webhooks |
| rules:read | Read mail rules and test them |
| rules:write | Create, edit and reorder mail rules |
| connections:read | Read which mailboxes are connected |
| members:read | See who is in the workspace and what they hold |
| members:write | Add and remove people, and change what they can reach |
| roles:read | Read the roles this workspace defines |
| roles:write | Create, edit and delete roles |
| settings:read | Read mailbox settings, including the signature |
| settings:write | Change mailbox settings and the signature |
| keys:write | Replace its own secret without anyone opening the console |
| keys:read | Read the API keys it covers, their request log and their activity |
| keys:manage | Create, change, rotate and revoke keys, never wider than itself |
| forms:read | Read sign-up forms and the people who filled them in |
| forms:write | Create, edit, publish and delete sign-up forms |
| knowledge:read | Read and search the knowledge base the AI uses |
| knowledge:write | Add, edit, pin and remove knowledge base items |
| automations:read | Read automations, who is in them and how they perform |
| automations:write | Create, edit, publish and pause automations, and enroll contacts |
| tasks:read | Read assistant tasks, what each run did and what it asked to approve |
| tasks:write | Create, edit, run and pause assistant tasks, and answer what they ask to approve |
| connectors:read | See the apps connected for the workspace and what the assistant did through them |
| connectors:use | Have the assistant look things up and make changes in the apps connected for the workspace |
| connectors:write | Connect, pause and disconnect apps for the workspace, and set what the assistant may do in them |
| billing:read | See the plan and invoices |
| billing:write | Change the plan and payment details |
| account:read | Read your own profile, notification settings, workspaces, connected apps, invitations, signed-in devices, passkeys and security log |
| account:write | Change 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.
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.
Google Workspace