# Authentication

Every VS Engine endpoint (except `/status` and `/widgets`) requires a
per-client API key, delivered out-of-band by Insideformation.

## Header

```
X-API-Key: <client-key>
```

## Key scopes

| Key | Scope | Language |
| --- | ----- | -------- |
| `API_KEY_HEMMAKLUBBEN` | Swedish Allsvenskan + Superettan | `sv` |
| `API_KEY_JUNIBET` | 30+ leagues across Europe | `en` |
| `API_KEY_ADMIN` | `/pipeline/*`, `/cost`, `/alerts` | — |

The server selects a locale from the authenticated `clientId` — see
`src/middleware/auth.ts`. Clients never send `Accept-Language`
explicitly; locale is an attribute of the key, not of the request.

## Bulkhead

Client keys are strictly siloed. A Hemmaklubben key hitting a
Junibet-only route returns `404`; a Junibet key hitting a
Hemmaklubben-only league returns `404`. There is no shared state
between clients at serve time, even though they share the same cached
Kambi/ValueStats store. See [`bulkhead.md`](bulkhead.md).

## Rate limiting

Per-client token bucket applied via `src/middleware/auth.ts`. Buckets are
keyed on the resolved `clientId` for authenticated traffic, so two clients
behind the same egress IP (corporate proxy, NAT, load balancer) get
independent budgets and a noisy client cannot starve another. Unauthenticated
traffic on open endpoints still buckets by IP.

On saturation the server returns `429 Too Many Requests` with a `Retry-After`
header and a `bucket` field indicating whether the trip was keyed on
`client` or `ip`.

This keying moved from IP to `clientId` on 2026-04-12 as part of the audit
remediation (item N3).

## Errors

| Status | `error` code | Meaning |
| ------ | ------------ | ------- |
| `401`  | `missing_api_key` | Header absent |
| `403`  | `invalid_api_key` | Header present, key unknown |
| `404`  | `client_not_found` | League/match outside this client's bulkhead |
| `429`  | `rate_limited` | Token bucket empty |
