Errors
Every error is a small JSON problem object. The status tells you whether to fix, wait or retry.
The problem format
When a request fails, the API answers with an HTTP error status and a body of type application/problem+json:
| Field | What it holds |
|---|---|
type | A URI that names the kind of problem, such as https://api.sockodds.com/problems/404. Use it or status to decide what to do; you do not need to open it. |
title | A short summary that is the same for every problem of this kind, such as Not found. |
status | The HTTP status code, repeated in the body. |
detail | What went wrong with this request, in plain words, such as Invalid starts_after. |
instance | The path and query of the request that failed. |
Here is a request for an event id that is well formed but does not exist:
curl -sS "https://api.sockodds.com/v1/events/ev_000000000000" \
-H "Authorization: Bearer $SOCKODDS_API_KEY"const response = await fetch("https://api.sockodds.com/v1/events/ev_000000000000", {
headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
console.log(JSON.stringify(await response.json(), null, 2));import json, os, urllib.error, urllib.request
request = urllib.request.Request(
"https://api.sockodds.com/v1/events/ev_000000000000",
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
{
"type": "https://api.sockodds.com/problems/404",
"title": "Not found",
"status": 404,
"detail": "Event not found.",
"instance": "/v1/events/ev_000000000000"
}And a parameter the API cannot read. Times must be UTC, written like 2026-10-15T08:00:00Z:
curl -sS "https://api.sockodds.com/v1/odds?league=epl&starts_after=tomorrow" \
-H "Authorization: Bearer $SOCKODDS_API_KEY"const response = await fetch("https://api.sockodds.com/v1/odds?league=epl&starts_after=tomorrow", {
headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
console.log(JSON.stringify(await response.json(), null, 2));import json, os, urllib.error, urllib.request
request = urllib.request.Request(
"https://api.sockodds.com/v1/odds?league=epl&starts_after=tomorrow",
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
{
"type": "https://api.sockodds.com/problems/400",
"title": "Invalid request",
"status": 400,
"detail": "Invalid starts_after",
"instance": "/v1/odds?league=epl&starts_after=tomorrow"
}The statuses the API returns
| Status | Title | What it means | What to do |
|---|---|---|---|
| 400 | Invalid request | A parameter is malformed or out of range. detail names it. | Fix the request. Retrying it unchanged gets the same answer. |
| 401 | Unauthorised | The key is missing, malformed, unknown or rotated. | Check the Authorization header and your current key. See Authentication. |
| 401 | Access changed | The key was rotated, or its plan changed, while the request was in flight. | Retry with your active key. |
| 403 | Plan restriction | Your plan does not include this league or feature (history, snapshots, streaming). | Choose something your plan covers, or change plan on Pricing. |
| 404 | Not found | The event does not exist. | Check the id. Do not retry. |
| 426 | Upgrade required | You called the stream with plain HTTP. | Connect with a WebSocket. See Streaming. |
| 429 | Rate limit reached | Too many requests this minute. | Wait for the Retry-After header (in seconds), then retry. |
| 429 | Monthly allowance reached | Your plan's objects for this month are used. | Do not retry: it will not clear this month. Upgrade on Pricing. |
| 429 | Stream limit reached | This key already has its maximum of open streams. | Close a stream first. |
| 503 | Service unavailable | The API could not answer just now. | Retry after a short wait, backing off each time. |
Common 400 details
detail | Cause |
|---|---|
Invalid event ID. or Invalid event_id | The id is not in the ev_ format. See Identifiers. |
Invalid starts_after, Invalid starts_before, Invalid from, Invalid to | Not a UTC time ending in Z, or a date that does not exist. Offsets such as +10:00 are refused: convert to UTC first. |
Invalid league, Invalid markets, Invalid bookmakers, Invalid regions | A key with capitals or other characters a key cannot have, or more than 20 keys in one list. Keys are lower case, such as epl or total_goals. |
Invalid alternates | Use true or false. |
Invalid pagination | limit is outside the range for the route. See Pagination. |
Invalid cursor | The cursor came from a different query. Follow links.next unchanged, or start again from the first page. |
Invalid snapshots: request snapshots for one event. | Snapshots work on /v1/events/{event_id}/odds only. See History and snapshots. |
Choose a league for this plan. | On the free Developer plan, teams, players and results need a league. |
Use a valid event and a history window of at most 24 hours ... | The history window is too long, ends before it starts, or the event id is malformed. |
What is not an error
- A filter that matches nothing returns
200with an emptydatalist. A league key that is well formed but unknown, or a bookmaker that is not quoting, does the same. An empty list is not evidence that a price or result is zero. - A price older than ten minutes is left out of current responses, not reported as an error. See Reading prices.
- A path that does not exist returns 404 too, but its body is not a problem object. Check the path against the API reference.
Retrying safely
Only two kinds of failure are worth retrying on their own: 429 Rate limit reached, which sends a Retry-After header, and 503 Service unavailable. Everything else needs a change first.
- On
429withRetry-After, wait that many seconds before the next request. - A
429withoutRetry-Afteris the monthly allowance or the stream limit. Retrying will not help. - On
503, back off: wait one second, then two, then four, and give up after a few attempts. - Every request you send counts towards your per-minute limit, including ones refused with 400 or 403. Errors are not charged as objects. See Limits.
This program wraps requests in a small helper that does exactly that, then asks for an event that does not exist:
const API = "https://api.sockodds.com/v1";
async function get(path) {
for (let attempt = 1; ; attempt += 1) {
const response = await fetch(API + path, {
headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
if (response.ok) return response.json();
const problem = await response.json();
const retryAfter = response.headers.get("retry-after");
const retry = response.status === 503 || (response.status === 429 && retryAfter !== null);
if (!retry || attempt === 3) throw new Error(`${problem.status} ${problem.title}: ${problem.detail}`);
const seconds = retryAfter !== null ? Number(retryAfter) : 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}
}
try {
await get("/events/ev_000000000000");
} catch (error) {
console.log(error.message);
}import json, os, time, urllib.error, urllib.request
API = "https://api.sockodds.com/v1"
def get(path):
attempt = 1
while True:
request = urllib.request.Request(
API + path,
headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]},
)
try:
with urllib.request.urlopen(request) as response:
return json.load(response)
except urllib.error.HTTPError as error:
problem = json.load(error)
retry_after = error.headers.get("Retry-After")
retry = error.code == 503 or (error.code == 429 and retry_after is not None)
if not retry or attempt == 3:
raise RuntimeError(f"{problem['status']} {problem['title']}: {problem['detail']}")
time.sleep(float(retry_after) if retry_after is not None else 2 ** (attempt - 1))
attempt += 1
try:
get("/events/ev_000000000000")
except RuntimeError as error:
print(error)Output from a local copy of the API with invented test prices
404 Not found: Event not found.Errors on an open stream arrive as an error frame holding the same problem object, followed by a close code. See Streaming.