18+ Only  |  Gambling can be addictive — please gamble responsibly  |  Gambling Help: 1800 858 858  |  GambleAware

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.

(18+, gamble responsibly, Gambling Help 1800 858 858.)

Get a free API key →

This site contains wagering-related analysis and is intended for Australian users aged 18+. Gambling involves risk. Please gamble responsibly.