> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flexslot.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Scopes

> The complete catalog of OAuth scopes and what each grants

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

<Note>
  **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).
</Note>

### 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) |

<Tip>
  `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.
</Tip>

### 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:

```json theme={null}
{
  "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 theme={null}
HTTP/1.1 403 Forbidden
Content-Type: application/json
```

```json theme={null}
{
  "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.

<Steps>
  <Step title="First login: minimum scopes">
    `scope=decks:read` — just enough to show the user their decks.
  </Step>

  <Step title="User clicks Export">
    Now you need `exports:read`. Start a new flow with `scope=decks:read exports:read`.
  </Step>

  <Step title="User keeps using the app">
    The new token has both scopes. You don't lose the user's prior consent — Flexslot remembers it.
  </Step>
</Steps>

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

<Warning>
  **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.
</Warning>

## 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](/oauth/quickstart#step-7-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](https://flexslot.gg/feedback) — these docs and the scope catalog are kept in sync.
