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

# Passthrough API Authentication — Scopes, Sportsbook Grants, and Sport Grants

> Authenticate SportriX Passthrough API requests with an API key carrying the odds scope, a sportsbook grant, and per-sport grants. Covers the X-API-Key header, the key query parameter, and 401, 403, and 503 responses.

The Passthrough API uses the same API keys as the rest of SportriX. A key is accepted here when it is **active** and carries all three of:

1. the **`odds`** scope,
2. a grant for the **sportsbook** you are calling — `pinnacle` for `/v1/pinnacle`, and
3. a grant for the **sport** you are asking about — see [Per-sport grants](#per-sport-grants) below.

All conditions are required. A key with the `odds` scope but no `pinnacle` grant is rejected, a `pinnacle` key without the `odds` scope is rejected, and a key that holds `odds` and `pinnacle` but not the sport in question is rejected on any request that names that sport.

<Note>
  `odds` is a **single scope covering both prematch and live prices**. There is no separate live-odds permission to request.
</Note>

## Passing your key

### REST — `X-API-Key` header

```bash theme={null}
curl "https://passthrough.sportrix.ai/v1/pinnacle/odds?match=1632570636" \
  -H "X-API-Key: sk_your_api_key_here"
```

### WebSocket — header or `key` parameter

The key is checked during the handshake, before the connection is upgraded. Server-side clients should send the header:

```text theme={null}
wss://passthrough.sportrix.ai/v1/pinnacle/data
X-API-Key: sk_your_api_key_here
```

Browsers cannot set headers on a WebSocket handshake, so the key may also be passed as a query parameter:

```text theme={null}
wss://passthrough.sportrix.ai/v1/pinnacle/data?key=sk_your_api_key_here
```

The `key` parameter is accepted on REST endpoints too, but the header is preferred — query strings end up in proxy logs and browser history.

<Warning>
  A key in a query parameter is a key in your logs. Use `?key=` only where a header is genuinely impossible, and prefer proxying browser traffic through your own backend.
</Warning>

When both are present, the **header wins**.

## Sportsbook grants

Each sportsbook is passed through by its own stack under its own path prefix, and each is granted independently on your key. Today that is:

| Prefix         | Sportsbook grant |
| -------------- | ---------------- |
| `/v1/pinnacle` | `pinnacle`       |

A key granted one book cannot read another's data.

## Per-sport grants

On top of the sportsbook grant, a key carries an **allowlist of sports** within that sportsbook. Access is **deny by default** — a newly issued key with no sport grants sees an empty book, even if it holds `odds` and `pinnacle`.

<Warning>
  Sport ids in this allowlist are the **sportsbook's own** ids, not the SportriX fixture catalog ids. On Pinnacle, for example, `12` is E Sports, `3` is Baseball, and `29` is Soccer. The two id spaces are unrelated — matching them by number is coincidence, not correspondence. Use ids from [`GET /sports`](/passthrough/sports), never from the SportriX fixture API.
</Warning>

### How grants are enforced

The allowlist affects both REST and WebSocket. The rule is consistent: **list endpoints filter, explicit sport references reject**.

| Request                                               | Behaviour when the sport is not granted                                                                                 |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [`GET /sports`](/passthrough/sports)                  | Returns only the sports you hold. Ungranted sports are silently absent                                                  |
| [`GET /leagues`](/passthrough/leagues) (no `?sport=`) | Returns only leagues in sports you hold                                                                                 |
| [`GET /leagues?sport=<id>`](/passthrough/leagues)     | `403 {"error": "API key not permitted for sport <id>"}`                                                                 |
| [`GET /matches`](/passthrough/matches) (no `?sport=`) | Returns only matches in sports you hold, including with `?league=`                                                      |
| [`GET /matches?sport=<id>`](/passthrough/matches)     | `403 {"error": "API key not permitted for sport <id>"}`                                                                 |
| [`GET /odds?match=<id>`](/passthrough/market-odds)    | `403 {"error": "API key not permitted for sport <sport_id>"}` if the match belongs to an ungranted sport                |
| [`WS /data`](/passthrough/stream) `subscribe`         | `error` frame `"API key not permitted for sport <sport_id>"`; the socket stays open and other subscriptions continue    |
| [`WS /data`](/passthrough/stream) `subscribe_all`     | Only matches in granted sports are subscribed. `subscribe_all` with a `sport` for an ungranted sport subscribes nothing |

The reason lists filter rather than reject is that the honest answer to "what can I see?" is the granted subset. The reason an explicit `?sport=` or match id rejects with `403` is that silence would be indistinguishable from a bug — the set of sports a sportsbook offers is public, so refusing loudly is cheaper to support and leaks nothing.

### A worked example

A `pinnacle` key granted E Sports (`12`) and Soccer (`29`), but not Baseball (`3`):

```bash theme={null}
# Returns only sports 12 and 29
curl "https://passthrough.sportrix.ai/v1/pinnacle/sports" \
  -H "X-API-Key: sk_your_api_key_here"

# Returns matches in sports 12 and 29 only
curl "https://passthrough.sportrix.ai/v1/pinnacle/matches" \
  -H "X-API-Key: sk_your_api_key_here"

# 403 {"error": "API key not permitted for sport 3"}
curl "https://passthrough.sportrix.ai/v1/pinnacle/matches?sport=3" \
  -H "X-API-Key: sk_your_api_key_here"
```

<Tip>
  To have specific sports added to your key, contact [SportriX support](mailto:support@sportrix.ai) with the sportsbook and the sport ids from [`GET /sports`](/passthrough/sports).
</Tip>

## Authentication errors

Every failure is a JSON body with an `error` field:

| Status | Body                                                          | Cause                                                                                                                                                     |
| ------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `{"error": "invalid or inactive API key"}`                    | No key was sent, the key is unknown, or it has been deactivated                                                                                           |
| `403`  | `{"error": "missing required scope: odds"}`                   | Valid key, but the `odds` scope is not granted                                                                                                            |
| `403`  | `{"error": "API key not permitted for sportsbook: pinnacle"}` | Valid key with the `odds` scope, but not granted this book                                                                                                |
| `403`  | `{"error": "API key not permitted for sport <id>"}`           | Valid key with `odds` and the sportsbook grant, but the sport referenced in the request (via `?sport=` or a match id) is not on the key's sport allowlist |
| `503`  | `{"error": "auth backend unavailable"}`                       | The key index could not be reached; retry with backoff                                                                                                    |

<Note>
  A `503` is about the authentication backend, not your key. Treat it as transient and retry — do not disable the key or alert on it as a permission problem.
</Note>

## Unauthenticated endpoints

Two endpoints answer without a key, deliberately, so that a liveness probe does not need a credential:

| Endpoint            | Without a key                                |
| ------------------- | -------------------------------------------- |
| `GET /healthz`      | Liveness only — returns `ok`                 |
| `GET /v1/pinnacle/` | The route list, for checking host and prefix |

To know whether the **data** is live rather than just the process, read the `feed_healthy` flag on your WebSocket [heartbeat](/passthrough/stream).

## Key propagation

Keys are managed centrally and mirrored to each sportsbook stack, which caches the result briefly. A newly issued key, a scope change, or a deactivation takes effect within roughly **15 seconds** — not instantly.

<Tip>
  To have the `odds` scope, a sportsbook grant, or specific sport grants added to your key, contact [SportriX support](mailto:support@sportrix.ai). No key rotation is needed.
</Tip>
