AFL Odds API: Merge Two Market IDs Into a 124-Book Board
Search for an AFL odds API and you find two kinds of answer. One is a scraper pointed at a single Australian bookmaker. The other is a data contract with a sales call in front of it. Neither one tells you what sits on the board for a preliminary final on a Friday night in Melbourne.
We measured it. On 7 September 2026 we pulled every Aussie Rules fixture in the next nine days and counted the board on each one. A preliminary final carried 104 bookmakers. An AFLW round match carried 121. Across the eleven priced fixtures we saw 156 distinct bookmaker slugs.
Then we tried to rank those books by margin and the numbers refused to line up. The reason turned out to be structural, and it will break your parser before it breaks your model.
Aussie Rules ships two market IDs for one result
The market catalogue holds exactly two markets for sportId 31:
| marketId | marketName | Length | Outcome IDs |
|---|---|---|---|
| 311 | Winner | 2 | 311 (home), 312 (away) |
| 313 | 1X2 | 3 | 313 (home), 314 (draw), 315 (away) |
/v4/markets filtered to sportId 31, 7 September 2026.Both markets settle the same match. A draw in the AFL is rare, so some books price the result two ways and others hang a draw price beside it. The feed keeps whichever shape the bookmaker publishes.
On the Fremantle Dockers v Geelong Cats preliminary final, 78 books quoted market 311, 41 quoted market 313, and 26 of them quoted 313 and nothing else. That set includes the entire bet365 group of nine slugs, 1xBet, 22bet, Melbet, Tipico and PointsBet Australia. Read market 311 alone and you drop a quarter of the board, including the book most of your readers actually hold an account with.
This is the same trap we hit on boxing, where one fight ships under three market IDs. The fix is the same. Resolve on the market name, collect every ID that maps to it, and merge.
import os, requests
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
WINNER, ONE_X_TWO = "311", "313"
HOME = {WINNER: "311", ONE_X_TWO: "313"}
AWAY = {WINNER: "312", ONE_X_TWO: "315"}
DRAW = "314"
def two_way_quotes(payload):
"""One home/away price per bookmaker, from whichever market ID it uses."""
out = {}
for slug, book in payload["bookmakerOdds"].items():
if book.get("suspended") or book.get("bookmakerIsActive") is False:
continue
for market_id in (WINNER, ONE_X_TWO):
market = (book.get("markets") or {}).get(market_id)
if not market:
continue
prices = {}
for outcome_id, outcome in market["outcomes"].items():
p = outcome["players"]["0"]
if p["active"] and p["price"]:
prices[outcome_id] = p["price"]
home, away = prices.get(HOME[market_id]), prices.get(AWAY[market_id])
if home and away:
out[slug] = {"market": market_id, "home": home,
"away": away, "draw": prices.get(DRAW)}
break
return out
odds = requests.get(f"{BASE}/odds",
params={"apiKey": API_KEY,
"fixtureId": "id3100065674372626"}).json()
quotes = two_way_quotes(odds)
print(len(quotes), "books with a usable home/away pair")
print(sum(1 for q in quotes.values() if q["market"] == ONE_X_TWO), "of them on 313")
That returns 99 usable pairs on the preliminary final, 26 of which arrive on market 313. A parser reading one ID sees 73.
Fifteen books publish a three-way market with two legs
Here is the second half of the trap. Of the 41 books quoting market 313 on that fixture, 15 ship it with no draw outcome at all. PointsBet Australia, Fanatics, 1xBet, 22bet, Betwinner, Melbet, Fonbet and Tipico all publish a market the catalogue calls three-way and send two prices.
Sum the inverse prices on those and you get a margin near 5.8%. Sum a complete three-way and you get 7.9%. Rank the board on that number and the incomplete books sit at the top of your leaderboard while offering you a worse bet. The overround is only comparable across books that priced the same number of outcomes, so count the legs before you compute anything. Our vig calculator walkthrough covers the arithmetic; the leg count is the check that goes in front of it.
| Quote shape | Books | Median overround | Comparable to |
|---|---|---|---|
| Market 311, two legs | 73 | 6.71% | other two-leg quotes |
| Market 313, two legs | 12 | 5.82% | nothing on this board |
| Market 313, three legs | 26 | 7.91% | other three-leg quotes |
The draw price itself is the loosest number on the board. Across the 14 books with a complete three-way, it runs from 24.5 to 85.0, an implied probability between 4.08% and 1.18%. De-vig the three-way consensus and the draw lands at 1.35%, which is close to the rate AFL matches actually draw at. Nobody competes on that leg, so the spread stays wide.
121 quotes, 16 prices
Board width and opinion count are different measurements. Group each fixture’s quotes by their price tuple and the AFL board collapses harder than any sport we have measured:
| Fixture | Books | Usable pairs | Independent prices | Collapse |
|---|---|---|---|---|
| AFL: Fremantle v Geelong | 104 | 99 | 29 | 70.7% |
| AFL: Brisbane v Adelaide | 124 | 109 | 25 | 77.1% |
| AFLW: Hawthorn v Western Bulldogs | 121 | 121 | 20 | 83.5% |
| AFLW: Adelaide v Geelong | 121 | 121 | 18 | 85.1% |
| AFLW: Gold Coast v Collingwood | 119 | 118 | 16 | 86.4% |
| AFLW: Melbourne v Port Adelaide | 121 | 121 | 16 | 86.8% |
| AFLW: North Melbourne v Carlton | 121 | 121 | 16 | 86.8% |
On the preliminary final, 22 slugs quoted an identical 1.606 / 2.25 and another 16 quoted 1.56 / 2.29. Those two tuples are 38% of the usable pairs between them. Average the raw slug list and you hand one pricing desk 22 votes.
Our study of how many bookmakers a backtest needs put the knee of the accuracy curve at 25 independent books. An AFLW board sits at 16. Treat the AFL consensus as a thinner signal than the raw count suggests, and lean on the exchange price as your anchor rather than the mean.
from collections import defaultdict
groups = defaultdict(list)
for slug, q in quotes.items():
groups[(q["home"], q["away"])].append(slug)
print(f"{len(quotes)} quotes -> {len(groups)} independent prices "
f"({100 * (1 - len(groups) / len(quotes)):.1f}% collapse)")
for tup, slugs in sorted(groups.items(), key=lambda kv: -len(kv[1]))[:3]:
print(tup, len(slugs), slugs[:5])
The men’s and women’s competitions draw different bookmakers
We expected the AFLW round to run thinner than the men’s finals. It runs wider. Nine AFLW fixtures carried 116 to 121 books each; the two preliminary finals carried 104 and 124.
The populations barely match. 31 books priced the AFLW round and neither preliminary final. 35 books did the reverse. The Unibet group, along with neds.com.au, TABtouch, BetRivers, ATG and Svenska Spel, appeared on all nine women’s fixtures and on no men’s final. The bet365 group of nine slugs, Betway, FanDuel, Kalshi, PMU and Tipico appeared on both men’s finals and on no women’s fixture.
If you test coverage on one fixture and generalise, you will describe half the market. Sample both competitions.
| Bookmaker | Priced fixtures | Where |
|---|---|---|
| pointsbet.com.au | 11 of 11 | both, on market 313 with no draw leg |
| 1xbet, stake, superbet | 11 of 11 | both |
| betfair-ex | 10 of 11 | both, tightest price on the board |
| unibet.com.au, neds.com.au, tabtouch | 9 of 11 | AFLW only |
| bet365, kalshi, fanduel, betway | 2 of 11 | AFL finals only |
Sixty-two slugs appeared on all eleven fixtures. For the sharp reference price, Betfair Exchange and its four network clones (fair999-ex, orbitx, piwi247-ex, sharpxch) quoted 1.62 / 2.58 at a 0.49% overround, with Kalshi second at 2.00%. Those five exchange slugs carry one price between them, so count them once. PointsBet Australia was the only book on every fixture with an Australian licence attached.
The 80.0 that is not a price
One slug, betmgm.bet.br, published market 311 as 1.55 on Fremantle and 80.0 on Geelong. The 80.0 looks like a draw price that landed on the away outcome. It carries active: true, suspended: false and a fresh timestamp, so no flag catches it.
A best-price scanner without a guard reports a 3,393% improvement over the board median and sends you to a URL that will not take the bet. Band the price against the median before you rank:
from statistics import median
med_home = median(q["home"] for q in quotes.values())
med_away = median(q["away"] for q in quotes.values())
def sane(price, med):
return 0.5 * med <= price <= 1.5 * med
best_home = max((q["home"], s) for s, q in quotes.items() if sane(q["home"], med_home))
best_away = max((q["away"], s) for s, q in quotes.items() if sane(q["away"], med_away))
print("median", med_home, med_away) # 1.58 2.29
print("best ", best_home, best_away) # (1.64, 'spelet.lv') (2.58, 'sharpxch')
print("unguarded", max((q["away"], s) for s, q in quotes.items())) # (80.0, 'betmgm.bet.br')
The real best price beats the median by 3.8% on Fremantle and 12.7% on Geelong. That is a normal line shopping gain, not a bug, and it survives the guard.
What you get, and what you do not
| Scraping one Australian book | OddsPapi /v4/odds |
|
|---|---|---|
| Books per fixture | 1 | 104 to 124 |
| Market shapes handled | whatever that site renders | both market IDs in one payload |
| Independent prices | 1 | 16 to 29 after dedupe |
| Exchange price | separate account and API | betfair-ex in the same call |
| Price history | whatever you stored | /v4/historical-odds, free tier |
| Setup | headless browser, proxies, breakage | one API key |
Two honest limits. Aussie Rules carries the result market and nothing else: no line, no totals, no player props, because the catalogue defines two markets for the sport. And the state leagues run thinner than the national ones. On our pull, the AFL and AFLW fixtures held full boards while the VFL and WAFL matches on the same weekend had none open yet. Books post the national competitions first.
Pull it yourself
1. Authenticate
The key goes in the query string. There is no header, and the free tier covers everything in this post.
import os, time, requests
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
def get(path, **params):
params["apiKey"] = API_KEY
r = requests.get(f"{BASE}{path}", params=params, timeout=30)
if r.status_code == 429:
time.sleep(5)
r = requests.get(f"{BASE}{path}", params=params, timeout=30)
r.raise_for_status()
return r.json()
print(len(get("/sports")), "sports") # 69
2. Find the fixtures
Aussie Rules is sportId 31. Ask for a window rather than a date, and set to to the day after the last day you want.
from datetime import date, timedelta
today = date.today()
fixtures = get("/fixtures", sportId=31,
**{"from": today.isoformat(),
"to": (today + timedelta(days=9)).isoformat()})
for f in fixtures:
if f["hasOdds"]:
print(f["startTime"][:16], f["tournamentName"],
f["participant1Name"], "v", f["participant2Name"], f["fixtureId"])
That returned 14 fixtures across the AFL, AFLW, VFL and WAFL. hasOdds is a flag, not a depth signal, so probe /odds on a few before you trust it. We cover that discovery pattern in more detail in the soccer fixtures guide.
3. Read the board
One call per fixture returns every bookmaker. Sleep a second between calls and never run them in parallel.
boards = {}
for f in fixtures:
if not f["hasOdds"]:
continue
payload = get("/odds", fixtureId=f["fixtureId"])
books = payload.get("bookmakerOdds") or {}
if books:
boards[f["fixtureId"]] = payload
print(f["participant1Name"], "v", f["participant2Name"], len(books), "books")
time.sleep(2)
4. Resolve, dedupe, rank
Run two_way_quotes over each board, group on the price tuple, then compute the overround inside each leg-count class. Filter on the price-level active flag and the book-level suspended and bookmakerIsActive flags first. On this pull the board was 98.5% active, the highest rate we have recorded on any sport, and the nine AFLW fixtures came back at 100%. Aussie Rules books post a price and stand behind it.
def overround(q):
legs = [q["home"], q["away"]] + ([q["draw"]] if q["draw"] else [])
return sum(1 / p for p in legs) - 1, len(legs)
ranked = []
for slug, q in quotes.items():
margin, legs = overround(q)
if 0 < margin < 0.30: # drop the broken quotes
ranked.append((margin, legs, slug))
for margin, legs, slug in sorted(ranked)[:5]:
print(f"{slug:<20} {margin * 100:5.2f}% {legs} legs")
De-vig the deduped consensus for a fair price rather than trusting one book. The three de-vig methods apply unchanged, with one AFL-specific note: the two-way and three-way boards agree. Proportional de-vig puts Fremantle at 59.5% on the two-leg quotes and 59.2% on the three-leg ones.
5. Read the history
/v4/historical-odds is on the free tier and takes three bookmakers per call. It answers when a board opened, which no live call can.
hist = get("/historical-odds",
fixtureId="id3100065674372626",
bookmakers="bet365,pointsbet.com.au,kalshi")
for slug, book in hist["bookmakers"].items():
for market_id, market in book["markets"].items():
for outcome_id, outcome in market["outcomes"].items():
snaps = sorted(outcome["players"]["0"], key=lambda s: s["createdAt"])
changes, prev = 0, None
for s in snaps:
if s["price"] != prev:
changes, prev = changes + 1, s["price"]
print(f"{slug:<18} {market_id}/{outcome_id} {len(snaps):>3} snapshots "
f"{changes - 1:>2} moves opened {snaps[0]['createdAt'][:16]}")
The preliminary final board opened on 5 September at 16:21 UTC, five days and eighteen hours before the bounce. Finals boards cannot open earlier than that, because the semi-finals decide who plays. bet365 posted 1.60 / 29.0 / 2.35 at 17:58 UTC and had not moved a leg 37 hours later across 98 snapshots. PointsBet repriced Fremantle twice and Geelong three times. Kalshi moved its home price 62 times. Count price changes, not snapshots, or you will measure our polling instead of their trading.
FAQ
Which bookmakers carry AFL odds?
We counted 156 distinct slugs across eleven finals-week fixtures on 7 September 2026, with 104 to 124 on any single match. PointsBet Australia, 1xBet, Stake and Superbet priced all eleven. Betfair Exchange priced ten and posted the tightest number on the board at a 0.49% overround.
Does the AFL board carry lines and totals?
The Aussie Rules catalogue defines two markets, both of them result markets: 311 Winner as a two-way, and 313 1X2 with a draw. For handicaps, totals and player props, the deep menus live on soccer, NFL and basketball.
Why do two AFL bookmakers show different margins on the same match?
Check the leg count before you compare. Fifteen books publish market 313 with only two prices, so their overround sums to about 5.8% while a complete three-way sums to 7.9%. Compare quotes with the same number of outcomes, and dedupe on the price tuple first, because 70% to 87% of an AFL board repeats another slug’s exact prices.
Is AFLW odds data as deep as the AFL?
Deeper on the pull we took. Nine AFLW fixtures carried 116 to 121 bookmakers each against 104 and 124 on the two preliminary finals, and every AFLW price came back active. The book populations differ: 31 slugs priced the women’s round and neither men’s final, and 35 did the reverse.
Is historical AFL odds data free?
Yes. /v4/historical-odds is on the free tier. The preliminary final board opened on 5 September at 16:21 UTC and had logged 1,240 snapshots across three bookmakers by the time we pulled it. The endpoint takes three bookmakers per call, so loop and merge.
Get the board
An OddsPapi key is free and reads 350+ bookmakers across 69 sports, with historical snapshots and WebSocket streaming on the same tier. Grab one, run the resolver above against this week’s fixtures, and see both market IDs land in the same dictionary.