Access control

Scopes

Every credential — an sk_live_… key or an MCP grant — carries a list of scopes shaped family:level. A key gets its list when it is minted; a grant gets what you ticked on the consent screen. Each operation in the reference names the one it requires, and a credential without it is refused with an error that names it back.

The scopes

Account-wide

Apply to the account itself rather than to any one channel.

ScopeGrantsMeaning
account:read Read your profile Handle, display name and links. Every key and grant includes this.
webhooks:read List your webhooks Registered endpoints and their health — never their secrets.
webhooks:write Manage webhooks Register and remove endpoints that receive your account’s events.

Channels and alerts

Composing, sending and measuring what a channel says.

ScopeGrantsMeaning
channels:read Read your channels Alert channels, their settings and subscriber counts.
channels:write Manage your channels Create, edit, pause and delete alert channels.
alerts:read Read alert history Alerts, their reads and acknowledgements.
alerts:write Compose draft alerts Write and edit drafts. Never publishes anything.
alerts:send ! Send alerts that can ring phones Publish now, schedule, and cancel. An alert cannot be unsent.
analytics:read Read channel analytics Sends, reads, acknowledgements and subscriber trends.
subscribers:read Read subscriber records Pseudonymous device records — platform, locale, timezone. Never identities.

Implication: write covers read

x:write satisfies x:read, and alerts:send satisfies alerts:write and alerts:read. Implication is resolved when the check runs, never stored: a grant holds exactly what was approved, and the check widens it. So a key minted with only uploads:write lists uploads too — but its stored list still says exactly what you chose.

alerts:send is separate from alerts:write on purpose: composing drafts is not the authority to ring phones. Scheduling and cancelling both ARE that authority — a scheduled alert reaches phones with no further approval, and withdrawing a live evacuation notice changes what reaches them — so both verbs require it too. (Early mockups had a separate alerts:schedule; it folded into alerts:send, because scheduling is send authority with a timestamp on it.)

Presets

The key-creation form offers these as starting points. They are explicit lists, not aliases — there is deliberately no stored full value that would silently absorb every future scope into old credentials.

PresetPitchScopes
Read-only Sees everything, changes nothing. account:readwebhooks:readchannels:readalerts:readanalytics:readsubscribers:read
Compose only Writes drafts and reads the numbers. CANNOT send — nothing it holds reaches a phone. account:readchannels:readalerts:readalerts:writeanalytics:read
Alerts operator Run channels end to end: compose, send, measure. account:readchannels:readchannels:writealerts:readalerts:writealerts:sendanalytics:readsubscribers:read
Full access Everything the account can do over the API. account:readwebhooks:readwebhooks:writechannels:readchannels:writealerts:readalerts:writealerts:sendanalytics:readsubscribers:read

Channel-bound keys

A key may be bound to one channel at creation. A bound key sees exactly its pusher — GET /v1/pushers lists one channel, and every other channel answers not_found — and may carry only the operating scopes: account:read, channels:read, the three alerts levels, analytics:read and subscribers:read. Audio and webhook authority stay off it (they would outlive the binding's meaning), and so does channels:write: a bound key may operate its channel's alerts, not re-describe or delete the channel. The form clamps the selection and the server clamps it again.

Legacy credentials: full access, labeled

A key minted before scopes existed, and an OAuth grant that carries only identity scopes, keep doing everything they could — breaking every creator's cron job and every connected assistant on deploy day, for a permission they de-facto had, is not hygiene. The studio labels both “Full access — created before scopes” so owners can rotate deliberately. New keys always carry a non-empty list and new grants always carry capability scopes, so the legacy encoding stays unambiguous.

The refusal, in full

A missing scope is 403 with error.type: "insufficient_scope" — a different word from forbidden on purpose, because the remedies differ: forbidden means “not your role or your data”; this means “your credential was minted without this permission — mint a key with it, or reconnect the assistant approving it”. The RFC 6750 header rides beside it, which is the standard place an OAuth client library looks first:

$ curl -X PATCH https://drop.top/api/v1/pushers/$PUSHER_ID/alerts/$ALERT_ID \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{"action":"send"}'

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="alerts:send"
Content-Type: application/json

{
  "error": {
    "type": "insufficient_scope",
    "message": "This credential lacks alerts:send. Mint a key carrying it, or reconnect the assistant approving it.",
    "param": null
  }
}

Any operation can answer this way; the reference spells it out only where an operation needs more than its declared scope — sending, where send: true raises the requirement from alerts:write to alerts:send.