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:
| Parameter | What 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=true | Adds 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 objectsYour plan's allowance is on the pricing page, and your usage so far is in your 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.
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 ZealandRespect 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_oddsis the bookmaker's own decimal price, not adjusted or rounded by us.implied_probabilityis1 / 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-Afterheader 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.