Scopes are how your app tells Flexslot — and the user — exactly what it wants to do. Each scope is a short, colon-separated string. You request them as a space-separated list on the /authorize request:
scope=decks:read sideboards:write guides:read
The user sees a humanized version on the consent screen.
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
| Scope | Grants |
|---|
decks:read | List the user’s decks, read deck details, read cards, read deck metadata |
decks:write | Create, update, and delete the user’s decks. Modify cards. Does not include read access — also request decks:read to list or fetch decks. |
Sideboard guides
Gates the standalone sideboard-guide resource (/games/{game}/sideboard-guides) and the per-opponent matchup guides nested inside each one.
| Scope | Grants |
|---|
sideboards:read | List sideboard guides and read their contents, including the nested matchup guides (opponent archetype, sideboarding plan, play/draw notes) |
sideboards:write | Create, update, and delete sideboard guides and their matchup guides; attach/detach decks. Does not include read access — also request sideboards:read to list or fetch them. |
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:*.
| Scope | Grants |
|---|
guides:read | Read a deck’s in-deck strategy guide (premium guides are preview-truncated for non-owners without content access) |
guides:write | Create, replace, and delete a deck’s in-deck strategy guide. Does not include read access — also request guides:read to read it. |
Cards
| Scope | Grants |
|---|
cards:read | Look up cards in the Flexslot card database (autocomplete, batch fetch, card details) |
cards:read does not access any user-specific data. It’s the same read-only card data available on the public site. Most integrations need this alongside decks:read because deck contents reference cards.
Exports
| Scope | Grants |
|---|
exports:read | Generate Excel, DOCX, and PDF exports of the user’s decks and sideboard guides |
How scope strings combine
Request multiple scopes by separating them with spaces (which are %20 once URL-encoded):
scope=decks:read sideboards:read guides:read cards:read
Scopes are a flat vocabulary with no hierarchy. 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
For scope=decks:read sideboards:write, the consent screen shows:
Quickstart Test wants to:
- Read your decks
- Manage your sideboards (create, edit, delete)
Phrasing comes from a Flexslot-controlled catalog. You can’t customize the text.
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:write request never silently collapses to decks: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.
There are no per-scope toggles on the consent screen — the user cannot approve a subset. If any requested scope is not in the client’s registered 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:
{
"access_token": "flx_at_…",
"scope": "decks:read sideboards:write",
"...": "..."
}
Narrowing scope yourself on refresh
You can deliberately request a narrower token on a grant_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 only decks:read calling a decks:write endpoint gets the canonical Flexslot envelope — there is no WWW-Authenticate header on the Bearer lane:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "FLEXSLOT_INSUFFICIENT_SCOPE",
"message": "Caller does not have the required scope(s).",
"details": {
"required": ["decks:write"],
"granted": ["decks:read"]
}
}
When you see 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.
First login: minimum scopes
scope=decks:read — just enough to show the user their decks.
User clicks Export
Now you need exports:read. Start a new flow with scope=decks:read exports:read.
User keeps using the app
The new token has both scopes. You don’t lose the user’s prior consent — Flexslot remembers it.
This pattern has two big wins:
- 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
Don’t request scopes you might use someday. The minimum-necessary rule is enforced socially (users notice, write-ups happen) and reputationally. Pages with sprawling consent screens convert worse.
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 a 403 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 no profile 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.
Future scopes
We add scopes when we add public API surface. If you need access to a Flexslot capability that doesn’t have a scope yet, open a feedback ticket — these docs and the scope catalog are kept in sync.