# Best practices

Habits that keep your integration fast, cheap and correct.

## Keep your key on your server

Your key identifies your account and spends your allowance. Send it only in the `Authorization: Bearer` header, from code that runs on your server.

- Read it from an environment variable or a secrets manager, never from source code. Every sample on this site reads `SOCKODDS_API_KEY`.
- Never put it in browser JavaScript, a mobile app bundle or a URL. Anyone who can open your app can read a key shipped inside it.
- If your website or app needs prices, have your server call SockOdds and serve your own users from your server or your own database.
- If a key leaks, rotate it from your [account](https://sockodds.com/account/) straight away. The replacement key is shown once, so store it before you leave the page.

```bash
export SOCKODDS_API_KEY="paste your key here"
```

## Ask only for what you use

Usage is counted in objects. Each record in `data` counts as one object, and every bookmaker price inside an odds response counts as one more. One game with 24 bookmaker prices uses 25 objects; the same game narrowed to one market at one bookmaker might use 3.

Narrow every odds request with the filters you need:

| Parameter | What it does |
| --- | --- |
| `markets=` | Up to 20 market keys, such as `winner` or `handicap`. See [markets](https://sockodds.com/docs/markets/). |
| `bookmakers=` | Up to 20 bookmaker keys. See [bookmakers](https://sockodds.com/bookmakers/). |
| `regions=` | Bookmakers in up to 20 regions, such as `au`. |
| `limit=` | Events per page on list routes (25 by default, at most 100). |
| `starts_after=`, `starts_before=` | A start window, in UTC. Without one, lists start 48 hours ago. |
| `alternates=true` | Adds alternate lines. Leave it off unless you show them. |

This program counts the objects two requests for the same game would use:

```javascript
const base = "https://api.sockodds.com/v1/events/ev_000ckk0nc1tg/odds";
const headers = { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` };

// One object for the event, plus one for each bookmaker's price.
function objects(event) {
  let prices = 0;
  for (const market of event.markets) {
    for (const outcome of market.outcomes) prices += outcome.prices.length;
  }
  return 1 + prices;
}

for (const [label, query] of [
  ["Every market, every bookmaker", ""],
  ["Winner only, one bookmaker", "?markets=winner&bookmakers=sportsbet"],
]) {
  const body = await (await fetch(base + query, { headers })).json();
  console.log(`${label}: ${objects(body.data)} objects`);
}
```

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

base = "https://api.sockodds.com/v1/events/ev_000ckk0nc1tg/odds"
headers = {"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}


# One object for the event, plus one for each bookmaker's price.
def objects(event):
    prices = sum(len(outcome["prices"]) for market in event["markets"] for outcome in market["outcomes"])
    return 1 + prices


for label, query in [
    ("Every market, every bookmaker", ""),
    ("Winner only, one bookmaker", "?markets=winner&bookmakers=sportsbet"),
]:
    with urllib.request.urlopen(urllib.request.Request(base + query, headers=headers)) as response:
        body = json.load(response)
    print(f"{label}: {objects(body['data'])} objects")
```

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

```text
Every market, every bookmaker: 25 objects
Winner only, one bookmaker: 3 objects
```

Your plan's allowance is on the [pricing page](https://sockodds.com/pricing/), and your usage so far is in your [account](https://sockodds.com/account/).

## Follow links.next

List routes return one page at a time. When there is more, `links.next` holds the full URL of the next page; when there is no more, it is `null`. Request that URL exactly as given, with the same `Authorization` header, until it is `null`. Do not build cursors yourself or change the other parameters between pages: a cursor belongs to the query that made it. More on this in [pagination](https://sockodds.com/docs/pagination/).

```javascript
const headers = { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` };

async function getAll(url) {
  const rows = [];
  while (url) {
    const body = await (await fetch(url, { headers })).json();
    rows.push(...body.data);
    url = body.links.next; // null on the last page
  }
  return rows;
}

const teams = await getAll("https://api.sockodds.com/v1/teams?league=rlwc&limit=100");
console.log(`${teams.length} teams`);
for (const team of teams.slice(0, 3)) console.log(`${team.key}: ${team.name}`);
```

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

headers = {"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}


def get_all(url):
    rows = []
    while url:
        with urllib.request.urlopen(urllib.request.Request(url, headers=headers)) as response:
            body = json.load(response)
        rows.extend(body["data"])
        url = body["links"]["next"]  # None on the last page
    return rows


teams = get_all("https://api.sockodds.com/v1/teams?league=rlwc&limit=100")
print(f"{len(teams)} teams")
for team in teams[:3]:
    print(f"{team['key']}: {team['name']}")
```

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

```text
2 teams
australia_rugby_league: Australia
new_zealand_rugby_league: New Zealand
```

## Respect freshness

- **Every price says when it was seen.** Each outcome carries `observed_at`. Show it, or use it to decide whether a price is recent enough for what you do.
- **Old quotes are withheld.** A quote older than ten minutes is left out of current responses instead of being shown as current. A side with no price means we have no fresh quote for it, not that the bookmaker has stopped offering it, and never that its price is zero.
- **Prices are exactly as shown.** `decimal_odds` is the bookmaker's own decimal price, not adjusted or rounded by us. `implied_probability` is `1 / decimal_odds`, with the bookmaker's margin still in it.
- **Pre-match first.** SockOdds records pre-match prices first. It is not a low-latency in-play feed.

## Back off when you are told to

A `429` problem means one of two things, and its `title` says which:

- **Rate limit reached.** You made more requests this minute than your plan allows. The response has a `Retry-After` header in seconds. Wait that long, then try again.
- **Monthly allowance reached.** Your plan's objects for the month are used up. Retrying will not help until the allowance resets or you change plan.

A `503` means the service could not answer just then: retry after a short, growing delay. Do not retry other `4xx` problems unchanged; read the `detail` and fix the request. All problems use `application/problem+json`; see [errors](https://sockodds.com/docs/errors/) and [limits](https://sockodds.com/docs/limits/).

```javascript
const headers = { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` };
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function getWithBackoff(url, attempts = 4) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, { headers });
    const body = await response.json();
    const retryable = response.status === 503 || (response.status === 429 && body.title === "Rate limit reached");
    if (!retryable || attempt === attempts) return body;
    const wait = Number(response.headers.get("retry-after")) || 2 ** attempt;
    await sleep(wait);
  }
}

const body = await getWithBackoff("https://api.sockodds.com/v1/leagues?sport=rugby_league");
console.log(JSON.stringify(body, null, 2));
```

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

headers = {"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}


def get_with_backoff(url, attempts=4):
    for attempt in range(1, attempts + 1):
        try:
            with urllib.request.urlopen(urllib.request.Request(url, headers=headers)) as response:
                return json.load(response)
        except urllib.error.HTTPError as e:
            body = json.load(e)
            retryable = e.code == 503 or (e.code == 429 and body.get("title") == "Rate limit reached")
            if not retryable or attempt == attempts:
                return body
            time.sleep(int(e.headers.get("Retry-After") or 2 ** attempt))


body = get_with_backoff("https://api.sockodds.com/v1/leagues?sport=rugby_league")
print(json.dumps(body, indent=2))
```

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

```json
{
  "data": [
    {
      "key": "nrl",
      "name": "NRL",
      "sport": "rugby_league",
      "region": "au",
      "periods": [
        "full",
        "regulation",
        "half_1",
        "half_2"
      ]
    },
    {
      "key": "nrlw",
      "name": "NRL Women's",
      "sport": "rugby_league",
      "region": "au",
      "periods": [
        "full",
        "regulation",
        "half_1",
        "half_2"
      ]
    },
    {
      "key": "rlwc",
      "name": "Rugby League World Cup",
      "sport": "rugby_league",
      "region": "intl",
      "periods": [
        "full",
        "regulation",
        "half_1",
        "half_2"
      ]
    },
    {
      "key": "rlwcw",
      "name": "Rugby League World Cup Women's",
      "sport": "rugby_league",
      "region": "intl",
      "periods": [
        "full",
        "regulation",
        "half_1",
        "half_2"
      ]
    }
  ],
  "meta": {
    "count": 4,
    "cursor": null
  },
  "links": {
    "next": null
  }
}
```

## Store our ids

Every game, player, market and outcome has a stable id: `ev_` for events, `pl_` for players, `mk_` for markets and `oc_` for outcomes. Teams, leagues, sports, market types and bookmakers use readable keys such as `australia_rugby_league` or `sportsbet`. Store these with your own records:

- Store the event id with every price and bet you save, so you can fetch [results](https://sockodds.com/docs/results/) for it later.
- Store the outcome id with a bet. Results use the same `oc_` id, so settling is a direct match.
- Store keys and ids, not display names. The API matches on keys and ids; names are for display and can be corrected.

See [identifiers](https://sockodds.com/docs/identifiers/) for the formats.

## Find games from official ids

If your system already knows a game by its official id, look up our event instead of matching team names and start times. `/v1/events/lookup` takes ESPN event ids (`source=espn`), AFL match ids for AFLW (`source=afl`) and NRL match ids for the Rugby League World Cup (`source=nrl`). Send `ref` for one id, or `refs` for up to 100 at once.

```bash
curl -sS "https://api.sockodds.com/v1/events/lookup?source=nrl&refs=20261310110" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
```

```javascript
const response = await fetch("https://api.sockodds.com/v1/events/lookup?source=nrl&refs=20261310110", {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
console.log(JSON.stringify(await response.json(), null, 2));
```

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

request = urllib.request.Request(
    "https://api.sockodds.com/v1/events/lookup?source=nrl&refs=20261310110",
    headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]},
)
with urllib.request.urlopen(request) as response:
    print(json.dumps(json.load(response), indent=2))
```

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

```json
{
  "data": [
    {
      "source": "nrl",
      "ref": "20261310110",
      "event": {
        "id": "ev_000ckk0nc1tg",
        "name": "Australia v New Zealand",
        "league": {
          "key": "rlwc",
          "name": "Rugby League World Cup"
        },
        "scheduled_start": "2026-10-15T09:05:00.000Z",
        "status": "scheduled",
        "participants": [
          {
            "role": "away",
            "identity": {
              "key": "new_zealand_rugby_league",
              "name": "New Zealand"
            }
          },
          {
            "role": "home",
            "identity": {
              "key": "australia_rugby_league",
              "name": "Australia"
            }
          }
        ]
      }
    }
  ],
  "meta": {
    "count": 1,
    "cursor": null
  },
  "links": {
    "next": null
  }
}
```

With `refs`, ids that match nothing are left out, so compare the `ref` values that come back with the ones you sent. Each event returned counts as one object. Only official ids can be looked up, never a bookmaker's own listing id.

## Cache the catalogue

Sports, leagues, bookmakers, markets and teams change rarely. Each row they return counts as an object like any other, so read them once at start-up or once a day and keep them, rather than on every request. Event lists change more often: refresh them on a schedule that suits your product, and read prices only for the games you show.

## Plan for gaps

- A game, market or bookmaker can be missing from a response for a while. Handle an empty list or a missing side without failing.
- A result can be `partial`: some markets settle before others. Keep a bet pending until its outcome has a verdict.
- History goes back to 22 December 2025 for some leagues and to 8 October 2026 for the rest ([dates by league](https://sockodds.com/docs/history/#how-far-back-it-goes)). It records changes only, so a quiet market has few rows.
