# Scopes

# Scopes

A client holds one or more scopes, chosen by the club admin when it's created (or changed later). A token
inherits its client's scopes, optionally narrowed with `scope=` at the token request. Calling an endpoint
without the scope it requires is `403 API_SCOPE_MISSING`.

| Scope | Grants | Endpoints |
|---|---|---|
| `events:read` | the club's published events, event detail, race list | [`GET /api/v1/events`](/docs/api/events) · [`GET /api/v1/events/{eventId}`](/docs/api/event) · [`GET /api/v1/events/{eventId}/races`](/docs/api/races) |
| `results:read` | race results, event (series) results | [`GET /api/v1/races/{raceId}/results`](/docs/api/race-results) · [`GET /api/v1/events/{eventId}/results`](/docs/api/event-results) |

Both are read-only — there is no write scope today. New endpoint families get new scopes as they ship; an
existing scope never silently widens to cover new data.

## Checking a token's scopes

Full reference: [Check a token](/docs/api/token-info).

```bash
curl -s https://sail-club-server.cloud.run/api/v1/token-info \
  -H "Authorization: Bearer $TOKEN"
```

```json
{ "client_id": "scc_3f9a…", "club_id": "TRX1001CL", "scope": "events:read results:read", "expires_at": "2026-09-30T10:00:00Z" }
```

Any valid token can call this — it needs no scope of its own, and is never rate-limited the way data calls
are. Useful for a health check or for debugging "why am I getting 403s."

## Narrowing a request

```bash
curl -s -X POST https://sail-club-server.cloud.run/api/v1/oauth/token \
  -u "$SAIL_CLUB_CLIENT_ID:$SAIL_CLUB_CLIENT_SECRET" \
  -d grant_type=client_credentials -d "scope=events:read"
```

Requesting a scope the client doesn't hold is `400 invalid_scope` (`API_SCOPE_INVALID`). Omitting `scope`
entirely returns a token with every scope the client has.

## Changing scopes

The club admin can add or remove scopes for a client at any time (Settings → API access → the client →
Edit). A removed scope is also removed from the client's **live** tokens within 60 seconds — you don't need
to wait for the token to expire to see the change.
