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:

FieldWhat it holds
typeA 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.
titleA short summary that is the same for every problem of this kind, such as Not found.
statusThe HTTP status code, repeated in the body.
detailWhat went wrong with this request, in plain words, such as Invalid starts_after.
instanceThe 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

StatusTitleWhat it meansWhat to do
400Invalid requestA parameter is malformed or out of range. detail names it.Fix the request. Retrying it unchanged gets the same answer.
401UnauthorisedThe key is missing, malformed, unknown or rotated.Check the Authorization header and your current key. See Authentication.
401Access changedThe key was rotated, or its plan changed, while the request was in flight.Retry with your active key.
403Plan restrictionYour plan does not include this league or feature (history, snapshots, streaming).Choose something your plan covers, or change plan on Pricing.
404Not foundThe event does not exist.Check the id. Do not retry.
426Upgrade requiredYou called the stream with plain HTTP.Connect with a WebSocket. See Streaming.
429Rate limit reachedToo many requests this minute.Wait for the Retry-After header (in seconds), then retry.
429Monthly allowance reachedYour plan's objects for this month are used.Do not retry: it will not clear this month. Upgrade on Pricing.
429Stream limit reachedThis key already has its maximum of open streams.Close a stream first.
503Service unavailableThe API could not answer just now.Retry after a short wait, backing off each time.

Common 400 details

detailCause
Invalid event ID. or Invalid event_idThe id is not in the ev_ format. See Identifiers.
Invalid starts_after, Invalid starts_before, Invalid from, Invalid toNot 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 regionsA 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 alternatesUse true or false.
Invalid paginationlimit is outside the range for the route. See Pagination.
Invalid cursorThe 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 200 with an empty data list. 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 429 with Retry-After, wait that many seconds before the next request.
  • A 429 without Retry-After is 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.

Need help?FAQ · Email support

Search across the SockOdds documentation and catalogue.