Skip to content

Scopes and reach

A key carries two limits. Both are checked on every call, and neither substitutes for the other.

Scopes

Scopes say what kind of call is allowed.

ScopeAllows
emails:sendPOST /emails, /emails/batch
emails:readListing and reading sent mail
mail:readThreads, messages, attachments, raw messages
mail:writeMoving, reading, starring, labelling, replying, deleting
mailboxes:readListing and reading mailboxes
mailboxes:writeCreating, changing and deleting them
domains:readListing and reading domains and their DNS
domains:writeAdding, re-checking and removing them
labels:readListing and reading labels
labels:writeCreating, renaming and deleting them
contacts:readListing contacts
suppressions:readListing blocked addresses
suppressions:writeBlocking and unblocking
webhooks:readListing endpoints and deliveries
webhooks:writeCreating, changing, testing, replaying
stats:readGET /stats
*All of the above, and anything added later

Writing implies reading. A key with mail:write has mail:read without being given it, because "may change but may not see" is a trap that only ever shows up as a 403 in production.

A missing scope is a 403:

json
{
  "error": "This API key does not have the \"mail:write\" scope",
  "code": "forbidden",
  "required_scope": "mail:write",
  "scopes": ["mail:read", "emails:send"]
}

Reach

Reach says which mail those calls may touch.

ReachMeans
EverythingEvery mailbox in the account, including ones made later
Whole domainsEvery address on those domains, including ones made later
Named addressesExactly those mailboxes

Reach is enforced everywhere, not only on sending:

  • GET /threads returns only threads in mailboxes the key reaches.
  • GET /domains returns only domains it holds.
  • GET /contacts returns 403 for any key that is not unrestricted, because contacts are kept per account and there is no honest way to narrow them.
  • POST /domains returns 403 for the same reason — adding a domain changes the account, not one corner of it.
  • A webhook cannot be created without a mailbox by a key that does not reach the whole account, or it would be a way to receive mail the key cannot read.

Something out of reach is a 404, not a 403

json
{ "error": "No such thread", "code": "not_found" }

Deliberately. A 403 would confirm the object exists, which lets somebody map what they cannot see by asking about it.

Sending, specifically

The key holdsMay send as
EverythingAny address on any verified domain. The mailbox is made on first use
Whole domainsAny address on those domains. The mailbox is made on first use
Named addressesExactly those addresses

This is why a whole-domain key is the right choice for transactional mail: receipts@, noreply@ and alerts@ all work without anybody creating them first.

Picking one

Give each integration the narrowest key that does its job.

The jobScopesReach
A billing service sending receiptsemails:sendthe billing. domain
A dashboard showing delivery healthemails:read, stats:readeverything
A bot that answers support mailmail:read, mail:write, emails:sendsupport@
A backup scriptmail:readeverything

Released under the MIT Licence.