Track sports betting line movement in Python (with a free odds API)
Where a price *moves* often tells you more than where it sits. A team shortening across every book is "steam" — money pushing the line. A drifting price is the opposite. Tracking that movement is the backbone of closing-line-value (CLV) analysis, steam-chasing, and a lot of betting models.
In this tutorial we'll pull live line movement and full price history across Australian bookmakers in Python, using the free PuntersEdge AU Odds API — no scraping required.
⚠️ 18+ only. This is about market data and probability, not a betting service. Odds move fast; past movement doesn't predict results. Bet only what you can afford to lose. Not financial advice. Gambling Help: 1800 858 858.
Get a free key
Grab a free key at puntersedge.online/api-platform (1,500 credits/month, no card). It goes in the X-API-Key header.
import requests
BASE = "https://api.puntersedge.online/v1"
HEADERS = {"X-API-Key": "YOUR_FREE_KEY"}
Step 1 — pull recent line movements
/v1/sports/{sport}/odds/movements returns recent price changes per bookmaker and market, with from, to and delta for each side:
def movements(sport):
r = requests.get(f"{BASE}/sports/{sport}/odds/movements", headers=HEADERS, timeout=15)
r.raise_for_status()
return r.json()
for m in movements("afl")[:5]:
ch = m["changes"]
print(f"{m['home_team']} v {m['away_team']} @ {m['bookmaker']}")
print(f" home {ch['home_price']['from']} -> {ch['home_price']['to']} "
f"({ch['home_price']['delta']:+})")
Step 2 — surface the biggest moves ("steam")
Filter for meaningful shifts and rank them — a quick steam detector:
def steam(sport, min_delta=0.15):
hits = []
for m in movements(sport):
for side in ("home_price", "away_price"):
d = m["changes"][side]["delta"]
if abs(d) >= min_delta:
hits.append((abs(d), m["home_team"], m["away_team"],
m["bookmaker"], side, d))
return sorted(hits, reverse=True)
for mag, home, away, book, side, d in steam("afl")[:10]:
arrow = "shortening" if d < 0 else "drifting"
print(f"{home} v {away} {side.split('_')[0]} {arrow} {d:+} @ {book}")
A price moving the *same way across multiple books* is far more meaningful than a single book twitching — group by event to confirm.
Step 3 — full price history & closing-line drift
/v1/sports/{sport}/odds/history returns every recorded snapshot per bookmaker, so you can chart open-to-current drift or compute CLV later:
def history(sport):
r = requests.get(f"{BASE}/sports/{sport}/odds/history", headers=HEADERS, timeout=15)
r.raise_for_status()
return r.json()
ev = history("afl")[0]
snaps = sorted(ev["snapshots"], key=lambda s: s["recorded_at"])
first, last = snaps[0], snaps[-1]
print(f"{ev['home_team']} v {ev['away_team']}")
print(f" {first['bookmaker']} open: {first['home_price']} -> latest: {last['home_price']}")
From here you can persist snapshots to a database, compute drift per event, or compare each book's price to the market average to spot the sharp ones.
Wrap up
With one endpoint you've got a live steam detector; with two you've got the data for CLV and movement modelling. Add a polling loop and a delta threshold and you've got alerts.
- Free key + docs: https://puntersedge.online/api-platform
- Also on RapidAPI: https://rapidapi.com/Propertyscout001/api/puntersedge-au-odds
- Endpoints:
/v1/sports/{sport}/odds/movements,/v1/sports/{sport}/odds/history
(18+, gamble responsibly, Gambling Help 1800 858 858.)