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

# Error Reference

> Every OAuth error code, what causes it, and how to fix it

Errors from the **OAuth endpoints** (`/token`, `/introspect`, `/revoke`) follow [RFC 6749 §5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2); the **authorization endpoint** redirects with [§4.1.2.1](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1) errors. Their shape:

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "The authorization code is expired or has already been used."
}
```

The **resource server** (`/api/public/v1/games/{game}/...`) instead returns Flexslot's canonical envelope (`{ "error": "FLEXSLOT_*", "message", "details" }`) and sends no `WWW-Authenticate: Bearer` header — see [Errors from the resource server](#errors-from-the-resource-server-api-public-v1) below. (Flexslot does not emit an `error_uri` field on any error.)

Most errors are programmer errors — a bad request, a misconfigured client. A few are user-driven (`access_denied`) and a few are signals of something seriously wrong (`invalid_grant` on a fresh code).

## Errors from `/authorize`

When the user lands on the authorization endpoint with a broken request, Flexslot redirects them back to your `redirect_uri` with `error=...` in the query string — **unless** the `redirect_uri` itself is invalid, in which case Flexslot shows an error page instead (so we never redirect to a wrong location).

| Error code                  | HTTP     | Meaning                                                     | Fix                                                                                           |
| --------------------------- | -------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `invalid_request`           | redirect | Required parameter missing or malformed                     | Check `response_type`, `client_id`, `redirect_uri`, `code_challenge`, `code_challenge_method` |
| `unauthorized_client`       | redirect | This `client_id` is not allowed to use `response_type=code` | Check the client's `grant_types` in partner admin                                             |
| `access_denied`             | redirect | The user clicked Deny                                       | Show a friendly "you denied access" page                                                      |
| `unsupported_response_type` | redirect | `response_type` is not `code`                               | Use `response_type=code`                                                                      |
| `invalid_scope`             | redirect | Requested scope isn't in the client's allowed scopes        | Add the scope to the client in partner admin                                                  |
| `server_error`              | redirect | Flexslot bug                                                | Check status page; retry; report if persistent                                                |
| `temporarily_unavailable`   | redirect | AS is overloaded or in maintenance                          | Back off and retry                                                                            |
| (HTML error page)           | 400      | `client_id` is unknown OR `redirect_uri` doesn't match      | Fix client\_id; check exact-string match for redirect\_uri                                    |

### Example error redirect

```
GET https://your.app/oauth/callback?
  error=invalid_scope&
  error_description=Scope%20cards%3Awrite%20is%20not%20a%20valid%20scope&
  state=4f8a9c1e2b3d
```

<Warning>
  Always include `state` validation **before** you act on an error. An attacker could send a crafted callback with an `error=...` they chose. If `state` doesn't match, abort the whole flow.
</Warning>

## Errors from `/token`

The token endpoint returns JSON with appropriate HTTP status codes.

### `invalid_request` — 400

The request is missing a required parameter, includes an unsupported parameter, or is malformed.

**Common causes:**

* Missing `grant_type`
* Missing `code` on `authorization_code` grant
* Missing `refresh_token` on `refresh_token` grant
* Missing `redirect_uri` on `authorization_code` grant
* Wrong `Content-Type` (must be `application/x-www-form-urlencoded`)
* `redirect_uri` doesn't match the one used at `/authorize` byte-for-byte

**Example:**

```json theme={null}
{
  "error": "invalid_request",
  "error_description": "redirect_uri does not match the one used in the authorization request"
}
```

**Fix:** echo back the exact `redirect_uri` you sent on the `/authorize` request.

### `invalid_client` — 401

Client authentication failed.

**Common causes:**

* Wrong `client_id`
* Wrong `client_secret`
* Client exists but is disabled in partner admin
* Trying to authenticate as a confidential client without HTTP Basic
* Trying to omit `client_id` from a public client request

**Example:**

```http theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}
```

The token endpoint returns the RFC 6749 §5.2 JSON body at `401` — it does **not** send a `WWW-Authenticate` header.

**Fix:** verify credentials in the partner admin. If you suspect the secret was rotated, generate a new one.

<Note>
  `invalid_client` is **not** the same as "wrong password". It also fires on missing `client_id`, wrong authentication method, or a disabled client.
</Note>

### `invalid_grant` — 400

The provided authorization grant (code or refresh token) is invalid, expired, revoked, or doesn't match the redirect URI or PKCE verifier.

**Common causes:**

* Authorization code already used (codes are single-use)
* Authorization code expired (10 minutes)
* `code_verifier` doesn't hash to the original `code_challenge`
* `redirect_uri` differs from the original `/authorize` request
* Refresh token expired (30 days idle)
* Refresh token already rotated (you replayed an old one — grant is now revoked)

**Example:**

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "code_verifier does not match the stored code_challenge"
}
```

**Fix:** start a new authorization flow. If this happens on refresh, the grant was revoked (likely refresh token replay) — the user must re-consent.

<Warning>
  If you see `invalid_grant` repeatedly on refresh, you have a refresh token rotation bug. Two parallel refreshes both succeed, both get new tokens, but only one is stored — the next refresh uses a stale token and revokes the grant. Serialize refreshes.
</Warning>

### `unauthorized_client` — 400

The authenticated client is not authorized to use the requested grant type.

**Common causes:**

* Client only has `authorization_code` configured, but the request used `refresh_token`
* Client was changed to disable a grant type after issuing tokens

**Fix:** add the grant type to the client's allowed grants in partner admin.

### `unsupported_grant_type` — 400

The `grant_type` parameter is something Flexslot doesn't support.

**Supported values:**

* `authorization_code`
* `refresh_token`

**Common causes:**

* Sending `password` (forbidden by RFC 9700)
* Sending `client_credentials` (not supported — use a PAT)
* Typo

**Fix:** use one of the two supported grant types.

### `invalid_scope` — 400

A requested scope is unknown or not allowed for this client.

**Example:**

```json theme={null}
{
  "error": "invalid_scope",
  "error_description": "Scope 'admin:write' is not a valid scope"
}
```

**Fix:** check the [scope catalog](/oauth/scopes) and the client's allowed scopes in partner admin.

## Errors from the resource server (`/api/public/v1/...`)

When a request to the API fails because of token or scope issues, the response is the canonical Flexslot envelope (`{ "error": "FLEXSLOT_*", "message": "...", "details": { ... } }`). The Bearer lane does **not** emit an RFC 6750 `WWW-Authenticate: Bearer` challenge — read the `error` code from the body. (The only resource-server response with a `WWW-Authenticate` header is the DPoP lane; see below.)

### `FLEXSLOT_OAUTH2_TOKEN_EXPIRED` — 401

The access token is past its 1-hour TTL.

```http theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
```

```json theme={null}
{ "error": "FLEXSLOT_OAUTH2_TOKEN_EXPIRED", "message": "OAuth2 access token has expired.", "details": {} }
```

**Fix:** refresh the access token. If the refresh also fails with `invalid_grant`, redirect the user to re-authorize.

### `FLEXSLOT_OAUTH2_TOKEN_REVOKED` — 401

The access token (or its grant) was revoked.

```json theme={null}
{ "error": "FLEXSLOT_OAUTH2_TOKEN_REVOKED", "message": "OAuth2 access token has been revoked.", "details": {} }
```

**Fix:** redirect the user to re-authorize.

### `FLEXSLOT_AUTHENTICATION_FAILED` — 401

The token is unknown/malformed, the user is inactive, the grant was revoked, or a DPoP-bound token was presented on the plain `Bearer` lane (downgrade defense — use the `DPoP` scheme instead).

```json theme={null}
{ "error": "FLEXSLOT_AUTHENTICATION_FAILED", "message": "Authentication failed.", "details": {} }
```

### `FLEXSLOT_INSUFFICIENT_SCOPE` — 403

The token is valid but doesn't have the scope this endpoint requires. The `details` object names the required and granted scopes.

```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"] }
}
```

There is no scope hierarchy — `decks:write` does **not** satisfy a `decks:read` requirement. **Fix:** start a new authorization flow with the additional scope. **Don't retry the same call with the same token** — it will fail every time. See [Scopes → Incremental authorization](/oauth/scopes#incremental-authorization).

### Missing `Authorization` header — 401

```json theme={null}
{ "error": "FLEXSLOT_AUTHENTICATION_REQUIRED", "message": "Authentication credentials were not provided.", "details": {} }
```

**Fix:** add `Authorization: Bearer <access_token>`. The header name is case-insensitive but the scheme (`Bearer`) is case-sensitive in practice on some proxies — use the canonical capitalization.

### DPoP lane — `WWW-Authenticate: DPoP error="invalid_dpop_proof"` — 401

The only resource-server response carrying a `WWW-Authenticate` header. Emitted on the `Authorization: DPoP …` scheme when the proof is missing/malformed/stale, the `ath` doesn't match, the `jti` was replayed, or the proof's JWK thumbprint doesn't match the token's bound key. Body: `{ "error": "FLEXSLOT_AUTHENTICATION_FAILED", ... }`. See [DPoP → errors](/oauth/dpop#dpop-errors).

## Errors from `/revoke`

[RFC 7009](https://www.rfc-editor.org/rfc/rfc7009) deliberately defines revocation to return `200` for both successful revocation and revocation of an already-invalid token. This prevents using `/revoke` as an oracle for token validity.

The only errors you'll see:

| Error             | HTTP | Meaning                   |
| ----------------- | ---- | ------------------------- |
| `invalid_client`  | 401  | Bad client credentials    |
| `invalid_request` | 400  | Missing `token` parameter |

## Errors from `/introspect`

[RFC 7662](https://www.rfc-editor.org/rfc/rfc7662). When the introspection succeeds (even for an invalid token), you get `200` with `{"active": false}` or `{"active": true, ...}`. You only see HTTP errors for client-auth failures.

```json theme={null}
{
  "active": false
}
```

```json theme={null}
{
  "active": true,
  "scope": "decks:read sideboards:read",
  "client_id": "flx_client_01HX2K…",
  "username": "user_01HX2K…",
  "exp": 1717180800,
  "iat": 1717177200
}
```

## Debugging checklist

When a flow fails and you're not sure why:

<Steps>
  <Step title="Check the error_description">
    It's always populated and is usually specific. Read it before assuming.
  </Step>

  <Step title="Diff your /authorize and /token redirect_uri">
    Most `invalid_request` cases are a trailing slash, scheme mismatch, or stray query string.
  </Step>

  <Step title="Confirm PKCE state">
    `code_verifier` from the same session as the `code_challenge`. Logging the first/last 4 chars of both (never the full value) on dev makes this obvious.
  </Step>

  <Step title="Check the clock">
    A skewed server clock can make a fresh code look expired. Run NTP.
  </Step>

  <Step title="Check parallel refreshes">
    Two requests refreshing the same token at once = one gets revoked. Add a per-user mutex.
  </Step>

  <Step title="Call /introspect on a token that's misbehaving">
    Confirms whether the token is `active`, what scopes it has, and when it expires.
  </Step>
</Steps>

<Tip>
  Never log token values, even truncated, in production. In development, log the first and last 4 characters only — enough to tell two tokens apart, not enough to use one.
</Tip>
