History and snapshots

Every change we recorded, and the first and last price before the start. Pro plans only.

Pro only

History and snapshots need a Pro key. Other plans get 403 Plan restriction. See Pricing.

What we record

We record a price each time it changes. A bookmaker showing the same price again does not add a row, so history is a list of changes, not a log of every look.

Only prices we recorded ourselves are served. Nothing is filled in: if we did not see a bookmaker for a while, that stretch has no rows. A gap means we have no observation, never that the price was missing or zero.

How far back it goes

v1 began recording every league on 8 October 2026. For the leagues below, our own earlier recordings were converted to v1 identities, so their history goes back further, to 22 December 2025 at the earliest. Every other league starts on 8 October 2026.

LeagueKeyHistory fromNot available
NFLnfl22 December 202527 January 2026 to 13 August 2026
NBAnba23 December 202529 January 2026 to 8 February 2026; 15 June 2026 to 2 October 2026
NBLnbl23 December 202521 February 2026 to 5 March 2026; 15 March 2026 to 24 September 2026
Men's college basketballncaab19 February 20267 April 2026 to 7 October 2026
English Premier Leagueepl22 February 20266 March 2026 to 13 March 2026; 23 March 2026 to 8 April 2026; 25 April 2026 to 13 August 2026
NHLnhl23 February 202616 June 2026 to 12 September 2026
MLBmlb24 March 20261 July 2026 to 28 July 2026
MLSmls14 August 2026None
La Ligala_liga17 August 202621 September 2026 to 1 October 2026
Bundesligabundesliga18 August 2026None
Ligue 1ligue_118 August 2026None
Serie Aserie_a18 August 2026None
College footballncaaf27 August 2026None
UEFA Champions Leaguechampions_league30 August 202611 September 2026 to 7 October 2026

Price changes for an event

GET /v1/odds/history returns the changes for one event in a time window.

ParameterRequiredMeaning
event_idYesThe event, such as ev_000ckk0nc1tg.
fromYesStart of the window, in UTC (2026-10-15T00:00:00Z). Included.
toYesEnd of the window, in UTC. Must be after from, and at most 24 hours later.
marketsNoOnly these market keys, comma separated, at most 20.
bookmakersNoOnly these bookmakers, comma separated, at most 20.
limitNoChanges per page: 250 by default, at most 1,000.
cursorNoSet for you in links.next.

Here are the first three winner market changes at one bookmaker over the day before the Rugby League World Cup opener. There are more, so links.next points to the next page:

curl -sS "https://api.sockodds.com/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-14T08:00:00Z&to=2026-10-15T08:00:00Z&markets=winner&bookmakers=sportsbet&limit=3" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
const response = await fetch("https://api.sockodds.com/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-14T08:00:00Z&to=2026-10-15T08:00:00Z&markets=winner&bookmakers=sportsbet&limit=3", {
  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/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-14T08:00:00Z&to=2026-10-15T08:00:00Z&markets=winner&bookmakers=sportsbet&limit=3",
    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": [
    {
      "event": "ev_000ckk0nc1tg",
      "market": {
        "id": "mk_qpcfjj74vm67qng3",
        "key": "winner",
        "name": "Winner",
        "period": "full",
        "subject": null
      },
      "outcome": {
        "id": "oc_2eq7ejh9tvepckre",
        "name": "Australia",
        "side": "home"
      },
      "bookmaker": "sportsbet",
      "observed_at": "2026-10-15T01:58:00.000Z",
      "kind": "opened",
      "decimal_odds": 1.79,
      "implied_probability": 0.558659,
      "line": null
    },
    {
      "event": "ev_000ckk0nc1tg",
      "market": {
        "id": "mk_qpcfjj74vm67qng3",
        "key": "winner",
        "name": "Winner",
        "period": "full",
        "subject": null
      },
      "outcome": {
        "id": "oc_43ctd7dv6hy4dqjz",
        "name": "New Zealand",
        "side": "away"
      },
      "bookmaker": "sportsbet",
      "observed_at": "2026-10-15T01:58:00.000Z",
      "kind": "opened",
      "decimal_odds": 2.2,
      "implied_probability": 0.454545,
      "line": null
    },
    {
      "event": "ev_000ckk0nc1tg",
      "market": {
        "id": "mk_qpcfjj74vm67qng3",
        "key": "winner",
        "name": "Winner",
        "period": "full",
        "subject": null
      },
      "outcome": {
        "id": "oc_2eq7ejh9tvepckre",
        "name": "Australia",
        "side": "home"
      },
      "bookmaker": "sportsbet",
      "observed_at": "2026-10-15T07:58:00.000Z",
      "kind": "changed",
      "decimal_odds": 1.72,
      "implied_probability": 0.581395,
      "line": null
    }
  ],
  "meta": {
    "count": 3,
    "cursor": "eyJhdCI6IjIwMjYtMTAtMTVUMDc6NTg6MDAuMDAwWiIsImlkIjoiODU0IiwicmV2aXNpb24iOiI4NzciLCJmIjoiOThkOTkzZjJlODllYTg0MiJ9"
  },
  "links": {
    "next": "https://api.sockodds.com/v1/odds/history?event_id=ev_000ckk0nc1tg&from=2026-10-14T08%3A00%3A00Z&to=2026-10-15T08%3A00%3A00Z&markets=winner&bookmakers=sportsbet&limit=3&cursor=eyJhdCI6IjIwMjYtMTAtMTVUMDc6NTg6MDAuMDAwWiIsImlkIjoiODU0IiwicmV2aXNpb24iOiI4NzciLCJmIjoiOThkOTkzZjJlODllYTg0MiJ9"
  }
}

Reading a change

Rows come oldest first. Each one names the event, the market (id, key, name, period, subject), the outcome (id, name, side) and the bookmaker, with the same ids as current prices, so you can join history to current prices and results.

FieldWhat it holds
observed_atWhen we saw the change at the bookmaker, in UTC.
kindopened: the first price we recorded for this outcome at this bookmaker, or its first price after a withdrawal. changed: a new price. withdrawn: the bookmaker took the selection down.
decimal_oddsThe new price, exactly as the bookmaker showed it. null when withdrawn.
implied_probability1 / decimal_odds, rounded to six places, or null.
lineThe line from this outcome's point of view, or null for a market without one.

A selection that comes back after a withdrawal shows as opened again. A bookmaker simply going quiet is not a withdrawal: only a selection the bookmaker took down is recorded as withdrawn.

Longer periods and paging

A window covers at most 24 hours, so read longer periods one day at a time. Inside a window, follow links.next unchanged until it is null, as on every list. A walk through one window reads the changes recorded when you started it: changes recorded while you page through do not shift rows between pages.

This program reads every change on the opener, across all markets and bookmakers, one day at a time and 20 changes a page, and counts each kind:

const API = "https://api.sockodds.com";
const headers = { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` };
const DAY = 24 * 60 * 60 * 1000;
const utc = (ms) => new Date(ms).toISOString().replace(".000Z", "Z");

const start = Date.parse("2026-10-14T00:00:00Z");
const end = Date.parse("2026-10-15T08:00:00Z");
const kinds = {};
for (let from = start; from < end; from += DAY) {
  const to = Math.min(from + DAY, end);
  let url = `${API}/v1/odds/history?event_id=ev_000ckk0nc1tg&from=${utc(from)}&to=${utc(to)}&limit=20`;
  let pages = 0;
  let changes = 0;
  while (url) {
    const response = await fetch(url, { headers });
    const body = await response.json();
    if (!response.ok) throw new Error(`${body.status} ${body.title}: ${body.detail}`);
    pages += 1;
    changes += body.data.length;
    for (const row of body.data) kinds[row.kind] = (kinds[row.kind] ?? 0) + 1;
    // Keep the path and query of links.next exactly as given; send them only to the API.
    const next = body.links.next ? new URL(body.links.next) : null;
    url = next ? API + next.pathname + next.search : null;
  }
  console.log(`${utc(from)} to ${utc(to)}: ${changes} changes in ${pages} ${pages === 1 ? "page" : "pages"}`);
}
console.log(Object.entries(kinds).map(([kind, n]) => `${kind} ${n}`).join(", "));
import json, os, urllib.request
from datetime import datetime, timedelta, timezone
from urllib.parse import urlsplit

API = "https://api.sockodds.com"
headers = {"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}
DAY = timedelta(days=1)


def utc(moment):
    return moment.strftime("%Y-%m-%dT%H:%M:%SZ")


start = datetime(2026, 10, 14, 0, 0, tzinfo=timezone.utc)
end = datetime(2026, 10, 15, 8, 0, tzinfo=timezone.utc)
kinds = {}
window_start = start
while window_start < end:
    window_end = min(window_start + DAY, end)
    url = f"{API}/v1/odds/history?event_id=ev_000ckk0nc1tg&from={utc(window_start)}&to={utc(window_end)}&limit=20"
    pages = 0
    changes = 0
    while url:
        with urllib.request.urlopen(urllib.request.Request(url, headers=headers)) as response:
            body = json.load(response)
        pages += 1
        changes += len(body["data"])
        for row in body["data"]:
            kinds[row["kind"]] = kinds.get(row["kind"], 0) + 1
        # Keep the path and query of links.next exactly as given; send them only to the API.
        next_link = body["links"]["next"]
        if next_link:
            parts = urlsplit(next_link)
            url = API + parts.path + ("?" + parts.query if parts.query else "")
        else:
            url = None
    print(f"{utc(window_start)} to {utc(window_end)}: {changes} changes in {pages} {'page' if pages == 1 else 'pages'}")
    window_start = window_end
print(", ".join(f"{kind} {n}" for kind, n in kinds.items()))

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

2026-10-14T00:00:00Z to 2026-10-15T00:00:00Z: 0 changes in 1 page
2026-10-15T00:00:00Z to 2026-10-15T08:00:00Z: 48 changes in 3 pages
opened 24, changed 24

Opening and closing prices

For one event's current prices, add snapshots=opening, snapshots=closing or snapshots=opening,closing to GET /v1/events/{event_id}/odds. Each outcome then carries:

FieldWhat it holds
openingThe first price we recorded for this outcome at this bookmaker before the scheduled start: decimal_odds, implied_probability and observed_at. null if we have none.
closingThe last price we recorded before the scheduled start, in the same shape. Before the start, it is the latest price so far. null if we have none, or if the selection was withdrawn before the start.

With snapshots, outcomes whose current price is stale or withdrawn stay in the response with available: false and null current prices, so you can still read their opening and closing prices after the game. Here is the closing line on the NFL game that has already been played:

curl -sS "https://api.sockodds.com/v1/events/ev_000ckk06psy5/odds?snapshots=closing&markets=winner&bookmakers=sportsbet" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
const response = await fetch("https://api.sockodds.com/v1/events/ev_000ckk06psy5/odds?snapshots=closing&markets=winner&bookmakers=sportsbet", {
  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/ev_000ckk06psy5/odds?snapshots=closing&markets=winner&bookmakers=sportsbet",
    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": {
    "id": "ev_000ckk06psy5",
    "name": "Arizona Cardinals v Atlanta Falcons",
    "league": {
      "key": "nfl",
      "name": "NFL"
    },
    "scheduled_start": "2026-10-12T08:00:00.000Z",
    "status": "final",
    "participants": [
      {
        "role": "away",
        "identity": {
          "key": "atlanta_falcons",
          "name": "Atlanta Falcons"
        }
      },
      {
        "role": "home",
        "identity": {
          "key": "arizona_cardinals",
          "name": "Arizona Cardinals"
        }
      }
    ],
    "markets": [
      {
        "id": "mk_w3g0d83k64amvfmc",
        "key": "winner",
        "name": "Winner",
        "period": "full",
        "subject": null,
        "alternate": false,
        "outcomes": [
          {
            "id": "oc_1dppq89hj6hzh2vs",
            "name": "Arizona Cardinals",
            "side": "home",
            "line": null,
            "prices": [
              {
                "bookmaker": "sportsbet",
                "decimal_odds": null,
                "implied_probability": null,
                "available": false,
                "closing": {
                  "decimal_odds": 1.72,
                  "implied_probability": 0.581395,
                  "observed_at": "2026-10-12T06:00:00.000Z"
                },
                "observed_at": "2026-10-12T06:00:00.000Z"
              }
            ]
          },
          {
            "id": "oc_cde9mrxgjh51dkzy",
            "name": "Atlanta Falcons",
            "side": "away",
            "line": null,
            "prices": [
              {
                "bookmaker": "sportsbet",
                "decimal_odds": null,
                "implied_probability": null,
                "available": false,
                "closing": {
                  "decimal_odds": 2.12,
                  "implied_probability": 0.471698,
                  "observed_at": "2026-10-12T06:00:00.000Z"
                },
                "observed_at": "2026-10-12T06:00:00.000Z"
              }
            ]
          }
        ]
      }
    ]
  },
  "meta": {
    "count": 1,
    "cursor": null
  },
  "links": {
    "next": null
  }
}

Snapshots work on one event at a time. /v1/odds refuses them with 400, so loop over the event ids you need.

Whole days as files

For research and model training, read history a whole day at a time instead of window by window. GET /v1/history/files lists one file per league and UTC day, from each league's first day above up to two days ago, and GET /v1/history/files/{league}/{day} downloads it as a Parquet file (zstd compressed) holding the same rows the history route returns for that league and day, sorted by event and observation time.

curl -sS "https://api.sockodds.com/v1/history/files?league=nfl&limit=2" \
  -H "Authorization: Bearer $SOCKODDS_API_KEY"
const response = await fetch("https://api.sockodds.com/v1/history/files?league=nfl&limit=2", {
  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/history/files?league=nfl&limit=2",
    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": [
    {
      "league": {
        "key": "nfl",
        "name": "NFL"
      },
      "day": "2026-10-12",
      "rows": 96,
      "bytes": 3836,
      "sha256": "a8aff56cc6923bc25c7ee51a008d55f68728fe7636d93923196a3fa723d433eb",
      "observed_from": "2026-10-12T00:00:00.000000Z",
      "observed_to": "2026-10-12T06:00:00.000000Z",
      "url": "https://api.sockodds.com/v1/history/files/nfl/2026-10-12"
    }
  ],
  "meta": {
    "count": 1,
    "cursor": null
  },
  "links": {
    "next": null
  }
}

Each listing gives the day's rows, bytes and sha256 (the download's ETag), and the first and last observation in it. Files need a plan with every bookmaker, so Pro. Listing counts one object per file listed and each download counts one object. At most two downloads at a time per key. The API reference lists the file's columns.

Use it for

  • Line movement: plot decimal_odds against observed_at. See Line movement.
  • Closing line value: compare the price you took with closing.
  • Audits: show the price a bookmaker had up at a given time, with the moment we saw it.
Need help?FAQ · Email support

Search across the SockOdds documentation and catalogue.