Scopes
A scope is a capability (resource:action), not a surface. The same scope
authorizes the same capability whether you arrive over REST or MCP.
Request only what you need — space-delimited in an OAuth authorize request, or selected per key in the admin center.
| Scope | Grants | Spends money |
|---|---|---|
sms:read | Texting configuration — which numbers can text, blocked senders, saved replies, schedules. Not message content. | No |
sms:write | Resolve and assign conversations; manage blocked senders, saved replies and drafts. Cannot send to anyone. | No |
sms:send | Send SMS from the account's numbers, and schedule one to go out later | Yes |
messages:read | Read message content — the inbox, a thread's text and media, delivery receipts, unread counts | No |
messages:write | Mark messages read or unread; permanently delete messages or whole conversations | No |
users:read | The account's users — names, emails, extensions. Personal data. | No |
recordings:read | Call recordings and transcripts — the contents of conversations | No |
webhooks:read | List and view webhook subscriptions | No |
webhooks:write | Create, update, delete, enable and disable subscriptions | No |
reports:read | Call history — who called whom, when, how long, how it ended | No |
The live list is scopes_supported in the
discovery document.
Why texting is five scopes
Because they are five different decisions:
- Building a number picker (
sms:read) should not require the ability to spend money. - A help-desk integration that only closes threads (
sms:write) should not be able to text customers. - A send-only integration (
sms:send) should not be able to enumerate the account's number inventory. - Reading message content is its own consent.
messages:readis separate fromsms:readfor the same reasonrecordings:readis separate from everything else: a thread is the contents of a conversation, not metadata about it. Folding it intosms:readwould also have silently upgraded every token already issued for a number picker into one that can read the account's entire message history. - Destroying messages is separate from bookkeeping.
sms:writeis reversible — a thread can be reopened, an assignment changed. A delete undermessages:writeis permanent.
Ask for each only if you do that thing.
:::note Deleting has a second gate that scopes do not override
messages:write lets you call the delete endpoints. If the account has
restrict deleting SMS switched on, they still answer 403 — that setting
exists so an account owner can stop their own staff and integrations from
destroying message history, and no scope overrides it.
:::
A scope is a ceiling, not a key
Holding a scope does not mean you get everything under it.
For an OAuth token, the portal permissions of the person who authorized your app still apply on top. A scope can only narrow what they could already do themselves — never widen it.
| Endpoint | Scope | …and the authorizing user's portal permission |
|---|---|---|
GET /users | users:read | Users — you see the users they can see, no more |
GET /sms/lines | sms:read | Phone numbers — only the numbers assigned to them |
POST /sms/messages | sms:send | Phone numbers — from must be one of theirs |
POST /sms/conversations/…/resolve, …/assign | sms:write | Phone numbers — the line must be one of theirs |
GET /sms/conversations, GET /sms/conversations/{line}/{number} | messages:read | Phone numbers — only threads on lines that are theirs |
DELETE /sms/conversations/… | messages:write | Phone numbers, plus the account's restrict-deleting-SMS setting |
POST /calls/search, GET /calls/{id} | reports:read | Call reports — only the calls their role exposes |
GET /calls/{id}/recordings, .../transcripts | recordings:read | Call reports → allow recordings — without it, 403 even with the scope |
So the same request can legitimately return different data for two tokens on the same account. If your app sees less than you expect, check the authorizing user's role in the portal before assuming a bug.
The two failures look alike and are fixed in different places:
error.type | What is wrong | Fixed by |
|---|---|---|
insufficient_scope | The credential lacks the scope | Your code — re-authorize |
permission_denied | The scope is fine; the role lacks the permission | Their admin, in the portal |
API keys are checked on scope only today — a key gets the whole account within its scopes. See What a key can reach.
Scopes a key can hold but OAuth cannot
flows:call — triggering an outbound call into a flow — is key-only and can
never be granted through the user-consent flow. Internal and service capabilities
stay out of reach of delegated third-party apps by design.