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 straight away. The replacement key is shown once, so store it before you leave the page.
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:

ParameterWhat it does
markets=Up to 20 market keys, such as winner or handicap. See markets.
bookmakers=Up to 20 bookmaker keys. See 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=trueAdds alternate lines. Leave it off unless you show them.

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

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`);
}
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

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

Your plan's allowance is on the pricing page, and your usage so far is in your account.

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.

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}`);
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

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 and limits.

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));
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

{
  "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 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 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.

curl -sS "https://api.sockodds.com/v1/events/lookup?source=nrl&refs=20261310110" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
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));
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

{
  "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). It records changes only, so a quiet market has few rows.
Need help?FAQ · Email support

Search across the SockOdds documentation and catalogue.