Track line movement

See how each bookmaker's price moved over a window, from recorded history.

Pro plan

Recorded history (/v1/odds/history) and opening and closing snapshots are part of the Pro plan. Other plans get a 403 problem. See pricing.

What it does

The program reads every recorded change to one game's winner market over an eight hour window, then prints, for each bookmaker and side, the first price seen in the window, the latest one and the difference.

History records changes only. A row has kind opened (the first quote we recorded), changed (a new price or line) or withdrawn (the quote was taken down, with a null price). Unchanged quotes add no rows, so a short list means a quiet market, not a gap. How far back history goes depends on the league: see History and snapshots.

The program

A history window can be at most 24 hours long. The program follows links.next until it is null, so it reads every page whatever the window holds.

const params = new URLSearchParams({
  event_id: "ev_000ckk0nc1tg",
  from: "2026-10-15T00:00:00Z",
  to: "2026-10-15T08:00:00Z",
  markets: "winner",
  limit: "250",
});
let url = `https://api.sockodds.com/v1/odds/history?${params}`;
const headers = { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` };

// First and latest price per bookmaker and outcome. Rows arrive oldest first.
const moves = new Map();
while (url) {
  const body = await (await fetch(url, { headers })).json();
  for (const row of body.data) {
    if (row.decimal_odds === null) continue; // a withdrawal has no price
    const key = `${row.bookmaker}|${row.outcome.side}|${row.market.period}`;
    const move = moves.get(key) ?? { bookmaker: row.bookmaker, name: row.outcome.name, first: row.decimal_odds, changes: 0 };
    move.latest = row.decimal_odds;
    move.at = row.observed_at;
    move.changes += row.kind === "changed" ? 1 : 0;
    moves.set(key, move);
  }
  url = body.links.next;
}

const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
for (const key of [...moves.keys()].sort(order)) {
  const m = moves.get(key);
  const diff = m.latest - m.first;
  const sign = diff > 0 ? "+" : "";
  console.log(
    `${m.bookmaker.padEnd(10)} ${m.name.padEnd(12)} ${m.first.toFixed(2)} to ${m.latest.toFixed(2)} ` +
      `(${sign}${diff.toFixed(2)}), changes: ${m.changes}, latest ${m.at}`,
  );
}
import json, os, urllib.parse, urllib.request

params = urllib.parse.urlencode({
    "event_id": "ev_000ckk0nc1tg",
    "from": "2026-10-15T00:00:00Z",
    "to": "2026-10-15T08:00:00Z",
    "markets": "winner",
    "limit": "250",
})
url = "https://api.sockodds.com/v1/odds/history?" + params
headers = {"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}

# First and latest price per bookmaker and outcome. Rows arrive oldest first.
moves = {}
while url:
    with urllib.request.urlopen(urllib.request.Request(url, headers=headers)) as response:
        body = json.load(response)
    for row in body["data"]:
        if row["decimal_odds"] is None:
            continue  # a withdrawal has no price
        key = f"{row['bookmaker']}|{row['outcome']['side']}|{row['market']['period']}"
        move = moves.setdefault(key, {"bookmaker": row["bookmaker"], "name": row["outcome"]["name"], "first": row["decimal_odds"], "changes": 0})
        move["latest"] = row["decimal_odds"]
        move["at"] = row["observed_at"]
        move["changes"] += 1 if row["kind"] == "changed" else 0
    url = body["links"]["next"]

for key in sorted(moves):
    m = moves[key]
    diff = m["latest"] - m["first"]
    sign = "+" if diff > 0 else ""
    print(
        f"{m['bookmaker'].ljust(10)} {m['name'].ljust(12)} {m['first']:.2f} to {m['latest']:.2f} "
        f"({sign}{diff:.2f}), changes: {m['changes']}, latest {m['at']}"
    )

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

ladbrokes  New Zealand  2.16 to 2.08 (-0.08), changes: 1, latest 2026-10-15T07:58:00.000Z
ladbrokes  Australia    1.75 to 1.69 (-0.06), changes: 1, latest 2026-10-15T07:58:00.000Z
playup     New Zealand  2.32 to 2.23 (-0.09), changes: 1, latest 2026-10-15T07:58:00.000Z
playup     Australia    1.88 to 1.81 (-0.07), changes: 1, latest 2026-10-15T07:58:00.000Z
sportsbet  New Zealand  2.20 to 2.12 (-0.08), changes: 1, latest 2026-10-15T07:58:00.000Z
sportsbet  Australia    1.79 to 1.72 (-0.07), changes: 1, latest 2026-10-15T07:58:00.000Z
tab        New Zealand  2.27 to 2.18 (-0.09), changes: 1, latest 2026-10-15T07:58:00.000Z
tab        Australia    1.84 to 1.77 (-0.07), changes: 1, latest 2026-10-15T07:58:00.000Z

In the test data every bookmaker shortened both sides between its first quote in the window and its latest one. changes counts the rows of kind changed.

A quicker way: opening and closing

If you only need the first and last quote, not every change, ask the event odds route for snapshots. snapshots=opening,closing adds an opening and a closing object to each bookmaker's price on each outcome, each with a price and observed_at. Before kickoff, closing is the latest quote so far. Snapshots work on one event at a time and are Pro only.

const url = "https://api.sockodds.com/v1/events/ev_000ckk0nc1tg/odds?markets=winner&snapshots=opening,closing";
const response = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.SOCKODDS_API_KEY}` },
});
const { data: event } = await response.json();
for (const market of event.markets) {
  for (const outcome of market.outcomes) {
    for (const price of outcome.prices) {
      const open = price.opening ? price.opening.decimal_odds.toFixed(2) : "none";
      const close = price.closing ? price.closing.decimal_odds.toFixed(2) : "none";
      console.log(`${price.bookmaker.padEnd(10)} ${outcome.name.padEnd(12)} opening ${open}, closing ${close}`);
    }
  }
}
import json, os, urllib.request

url = "https://api.sockodds.com/v1/events/ev_000ckk0nc1tg/odds?markets=winner&snapshots=opening,closing"
request = urllib.request.Request(
    url, headers={"Authorization": "Bearer " + os.environ["SOCKODDS_API_KEY"]}
)
with urllib.request.urlopen(request) as response:
    event = json.load(response)["data"]
for market in event["markets"]:
    for outcome in market["outcomes"]:
        for price in outcome["prices"]:
            opening = f"{price['opening']['decimal_odds']:.2f}" if price.get("opening") else "none"
            closing = f"{price['closing']['decimal_odds']:.2f}" if price.get("closing") else "none"
            print(f"{price['bookmaker'].ljust(10)} {outcome['name'].ljust(12)} opening {opening}, closing {closing}")

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

playup     Australia    opening 1.88, closing 1.81
tab        Australia    opening 1.84, closing 1.77
sportsbet  Australia    opening 1.79, closing 1.72
ladbrokes  Australia    opening 1.75, closing 1.69
playup     New Zealand  opening 2.32, closing 2.23
tab        New Zealand  opening 2.27, closing 2.18
sportsbet  New Zealand  opening 2.20, closing 2.12
ladbrokes  New Zealand  opening 2.16, closing 2.08

With snapshots requested, a quote that is stale or withdrawn can still appear so you can see its opening and closing. Its entry in prices has available set to false and a null decimal_odds, and sorts after the live prices, so check available before you treat a price as current.

Make it yours

  • Track a handicap or total: change markets= and add the row's market.id to the key. Each line is its own market with its own id, so a new line starts a new series.
  • Track a whole day: move the window. Each request covers up to 24 hours, so walk a longer period one day at a time.
  • Store rows as they come: keep observed_at, kind, the price and the outcome id, and you have your own copy of the market's movement.

See price history for the full rules and the API reference for every parameter. The output above comes from a local copy of the API with invented test prices.

Need help?FAQ · Email support

Search across the SockOdds documentation and catalogue.