/authorize request:
Catalog
Scopes are additive and minimum-necessary. Request only what you need for the operation in front of the user. You can request more scopes later via incremental authorization (start a new flow with the additional scopes).
Decks
Sideboard guides
Gates the standalone sideboard-guide resource (/games/{game}/sideboard-guides) and the per-opponent matchup guides nested inside each one.
Deck guides
Gates a deck’s own in-deck strategy guide (/games/{game}/decks/{id}/guide) — the Markdown/TipTap write-up attached to a single deck. This is not the sideboard-guide resource above; a “sideboard guide” is gated by sideboards:*, not guides:*.
Cards
Exports
How scope strings combine
Request multiple scopes by separating them with spaces (which are%20 once URL-encoded):
decks:write does not include decks:read — if your app both reads and writes decks, request both explicitly (scope=decks:read decks:write). The resource server checks each endpoint’s required scope with an exact subset match, so a token holding only decks:write gets 403 FLEXSLOT_INSUFFICIENT_SCOPE on any read endpoint.
What the user sees
Forscope=decks:read sideboards:write, the consent screen shows:
Quickstart Test wants to:Phrasing comes from a Flexslot-controlled catalog. You can’t customize the text.
- Read your decks
- Manage your sideboards (create, edit, delete)
Allow or Deny — there’s no partial grant
At the consent screen the user makes a single binary decision:- Allow — Flexslot grants exactly the scopes you requested (a
decks:writerequest never silently collapses todecks:read). The authorization code, and the access token minted from it, carry your full requested scope set. - Deny — you receive
?error=access_denied&error_description=...&state=...&iss=...at your redirect URI and no code is issued.
allowed_scopes, the whole request fails up front with error=invalid_scope (a redirect error before consent is ever shown).
The /token response still echoes the granted scope for forward-compatibility — read it rather than assuming — but on the authorization-code grant it equals what you requested:
Narrowing scope yourself on refresh
You can deliberately request a narrower token on agrant_type=refresh_token exchange by passing a scope param. The requested scopes must be a subset of the original grant; anything outside it returns error=invalid_scope. Omit scope to keep the full grant. This is the only path that produces a token with fewer scopes than the grant, and it is opt-in by the client — Flexslot never narrows on its own.
Scope enforcement on the API
The resource server checks scopes per-endpoint. A token with onlydecks:read calling a decks:write endpoint gets the canonical Flexslot envelope — there is no WWW-Authenticate header on the Bearer lane:
FLEXSLOT_INSUFFICIENT_SCOPE, read details.required for the missing scope. The fix is not to retry — it’s to start a new authorization flow that requests the missing scope.
Incremental authorization
Instead of requesting every possible scope on first login, start with the minimum and request more when you need them.1
First login: minimum scopes
scope=decks:read — just enough to show the user their decks.2
User clicks Export
Now you need
exports:read. Start a new flow with scope=decks:read exports:read.3
User keeps using the app
The new token has both scopes. You don’t lose the user’s prior consent — Flexslot remembers it.
- Trust: users are warier of apps that demand everything up front
- Recovery: if a scope check fails on the API, you have a clear path forward
Scope vs role
Scope is what the app may attempt. The user’s permissions on the specific resource still decide whether the action succeeds. When the action isn’t permitted, the resource server returns a403 with a non-FLEXSLOT_INSUFFICIENT_SCOPE error code — for example FLEXSLOT_NOT_DECK_OWNER when the caller tries to act on a deck the user doesn’t own.
User identity — no scope required
There is noprofile or email scope. Any OAuth access token can call GET /_probe/user to retrieve the connected user’s display identity — user.id (bare Firebase id, matches author.id on their decks) and user.username (the handle shown on the consent screen). That’s everything you need for a “Connected as …” UI, and it comes free with the grant — request only the resource scopes you actually act on (decks:read, etc.). The user’s email is not returned: the consent screen shows the username (it falls back to email only when a user has no username), so email isn’t something every user reliably approves sharing — exposing it to a decks:read-only partner would be undisclosed PII. See Identify the connected user.