Track line movement
See how each bookmaker's price moved over a window, from recorded history.
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.000ZIn 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.08With 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'smarket.idto 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.