# Authentication

One header on every request, and a key that never leaves your server.

## Send the key as a bearer token

Every request to `https://api.sockodds.com/v1` needs your key in the `Authorization` header:

```text
Authorization: Bearer <your key>
```

That header is the only place the API reads a key. A key in the query string or in any other header is ignored, and the request fails with 401. The WebSocket stream takes the same header on its upgrade request; see [Streaming](https://sockodds.com/docs/streaming/).

All the examples on this site read the key from the `SOCKODDS_API_KEY` environment variable, as the [quickstart](https://sockodds.com/docs/quickstart/) sets up.

## Keep the key on your server

Your key is tied to your plan and your monthly allowance. Anyone who has it can spend that allowance.

- Call the API from your own server, a serverless function or a scheduled job, never from code that runs in a browser or a mobile app. Anything shipped to a device can be read.
- Never put the key in a URL. URLs end up in logs, browser history and analytics.
- Keep it out of source control. Read it from an environment variable or your platform's secret store.
- When your front end needs prices, have it call your own server, and have your server call SockOdds.
- Never send your key to support. We will never ask for it.

## Rotate a lost or leaked key

Sign in to [your account](https://sockodds.com/account/) and choose **Rotate key**. Rotation disables the old key straight away and shows the replacement once, so copy it before you leave the page. Update every server that uses the key: requests with the old one fail with 401 from that moment.

If you closed checkout before your key was shown, sign in and choose **Recover a paid purchase**.

## 401: the key was not accepted

A 401 means the API could not accept your key: the header is missing, it is not in the `Bearer <key>` form, or the key is unknown or has been rotated. The body is a problem object, as on every error:

```bash
curl -sS "https://api.sockodds.com/v1/sports" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
```

```javascript
const response = await fetch("https://api.sockodds.com/v1/sports", {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
// Print the body whatever the status: on an error it is a problem object.
console.log(JSON.stringify(await response.json(), null, 2));
```

```python
import json, os, urllib.error, urllib.request

request = urllib.request.Request(
    "https://api.sockodds.com/v1/sports",
    headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]},
)
try:
    with urllib.request.urlopen(request) as response:
        body = json.load(response)
except urllib.error.HTTPError as error:
    body = json.load(error)  # the problem object
print(json.dumps(body, indent=2))
```

Response (from a local copy of the API with invented test prices):

```json
{
  "type": "https://api.sockodds.com/problems/401",
  "title": "Unauthorised",
  "status": 401,
  "detail": "Provide a valid bearer key.",
  "instance": "/v1/sports"
}
```

Check that the variable holds your current key, with no spaces or quotes around it. Retrying the same key will not help.

A 401 titled `Access changed` means your key was valid when the request arrived but was rotated, or its plan changed, before the response was sent. Retry with your active key.

## 403: your plan does not include this

A 403 means the key is fine but your plan does not cover what you asked for: a league outside the plan, or a Pro feature such as [history and snapshots](https://sockodds.com/docs/history/) or [streaming](https://sockodds.com/docs/streaming/). Here a free Developer key asks for price history:

```bash
curl -sS "https://api.sockodds.com/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-15T00:00:00Z&to=2026-10-15T08:00:00Z" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
```

```javascript
const response = await fetch("https://api.sockodds.com/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-15T00:00:00Z&to=2026-10-15T08:00:00Z", {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
console.log(JSON.stringify(await response.json(), null, 2));
```

```python
import json, os, urllib.error, urllib.request

request = urllib.request.Request(
    "https://api.sockodds.com/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-15T00:00:00Z&to=2026-10-15T08:00:00Z",
    headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]},
)
try:
    with urllib.request.urlopen(request) as response:
        body = json.load(response)
except urllib.error.HTTPError as error:
    body = json.load(error)
print(json.dumps(body, indent=2))
```

Response (from a local copy of the API with invented test prices):

```json
{
  "type": "https://api.sockodds.com/problems/403",
  "title": "Plan restriction",
  "status": 403,
  "detail": "History requires Pro access.",
  "instance": "/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-15T00:00:00Z&to=2026-10-15T08:00:00Z"
}
```

The `detail` says what the plan is missing. Retrying will not change the answer; [Pricing](https://sockodds.com/pricing/) shows which plan includes it.

Bookmakers work differently. On a plan with a set list of bookmakers, prices from other bookmakers are left out of the response rather than refused, so a request still succeeds with fewer bookmakers.

Every other error is described in [Errors](https://sockodds.com/docs/errors/).
