Cricket Odds API: IPL, T20 & Test Match Data from 20+ Bookmakers (Free Tier)
Cricket Odds API: Why Getting Programmatic Access Is Harder Than It Should Be
Cricket is the second-most bet sport on the planet. The IPL alone moves more money than most American leagues combined. But if you’re a developer trying to build anything — a model, a scanner, a dashboard — you’ll quickly discover the data ecosystem is a mess.
The big bookmakers (Pinnacle, Bet365, Betfair) don’t offer public APIs. The cricket-specific data providers (CricketAPI, EntitySport) give you scores and stats, not odds. And the enterprise feeds from Sportradar and LSports will quote you five figures before you see a JSON response.
OddsPapi sits in the middle: 20+ cricket bookmakers (including Pinnacle and the Betfair Exchange), 175+ live markets per fixture, and a free tier that doesn’t require a sales call. This guide walks you through pulling IPL, T20, and Test match odds with Python.
What You Get vs. What You’re Used To
| Scraping / Manual | Enterprise Feed (Sportradar, LSports) | OddsPapi | |
|---|---|---|---|
| Cricket Bookmakers | 1 at a time | 10–40 | 49 on IPL fixtures |
| Sharp Lines (Pinnacle) | No public API | Yes (enterprise only) | Yes (free tier) |
| Betfair Exchange Depth | Requires Betfair API key | Sometimes | Full back/lay + liquidity |
| Markets per Fixture | Match winner only | 50–100 | 175+ (overs, powerplay, runs) |
| Historical Odds | Not available | $$$ add-on | Free (backtest your models) |
| Price | Free (until you get blocked) | $5K–$50K/year | Free tier available |
Step 1: Authentication & Sport Discovery
Every OddsPapi request uses a query parameter for auth — no headers, no OAuth.
import requests
import time
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
# Verify your key works
response = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
sports = response.json()
# Find cricket
cricket = [s for s in sports if s["slug"] == "cricket"][0]
print(f"Sport: {cricket['name']} (ID: {cricket['sportId']})")
# Output: Sport: Cricket (ID: 27)
Important terminology: OddsPapi uses “tournament” where you might say “league,” “fixture” instead of “match,” and “participant” instead of “team.” The cricket sport ID is 27.
Step 2: Fetch Cricket Fixtures (IPL, PSL, T20, Tests)
The /fixtures endpoint returns upcoming and recent matches within a date range (max 10 days per call).
from datetime import datetime, timedelta
# Get the next 10 days of cricket fixtures
today = datetime.utcnow().strftime("%Y-%m-%d")
end = (datetime.utcnow() + timedelta(days=10)).strftime("%Y-%m-%d")
response = requests.get(f"{BASE_URL}/fixtures", params={
"apiKey": API_KEY,
"sportId": 27,
"from": today,
"to": end
})
fixtures = response.json()
# Filter to fixtures that actually have odds
with_odds = [f for f in fixtures if f.get("hasOdds")]
print(f"Total fixtures: {len(fixtures)}, With odds: {len(with_odds)}")
# Group by tournament
from collections import Counter
tournaments = Counter(f["tournamentName"] for f in with_odds)
for name, count in tournaments.most_common(10):
print(f" {name}: {count} fixtures")
A typical 10-day window gives you 50–60 fixtures with odds across the IPL, Pakistan Super League, Big Bash, Caribbean Premier League, international T20s, and domestic competitions.
Tip: Only fixtures with hasOdds: true will return pricing from the /odds endpoint. The rest are metadata-only.
Step 3: Pull Live Odds — Match Winner
The main cricket market is “Winner (incl. super over)” — market ID 271. This is your match-winner line.
# Pick an IPL fixture
ipl_fixtures = [f for f in with_odds if "Indian Premier League" in f.get("tournamentName", "")]
if not ipl_fixtures:
print("No IPL fixtures in range — try a different date window")
else:
fixture = ipl_fixtures[0]
fid = fixture["fixtureId"]
print(f"{fixture['participant1Name']} vs {fixture['participant2Name']}")
print(f"Fixture ID: {fid}")
time.sleep(0.2)
# Get odds from all bookmakers
odds_response = requests.get(f"{BASE_URL}/odds", params={
"apiKey": API_KEY,
"fixtureId": fid
})
odds_data = odds_response.json()
# Parse match-winner prices (market 271)
MATCH_WINNER = "271"
for slug, bookie_data in odds_data["bookmakerOdds"].items():
markets = bookie_data.get("markets", {})
if MATCH_WINNER in markets:
outcomes = markets[MATCH_WINNER]["outcomes"]
home = outcomes.get("271", {}).get("players", {}).get("0", {})
away = outcomes.get("272", {}).get("players", {}).get("0", {})
print(f" {slug:20s} {home.get('price', '-'):>6} {away.get('price', '-'):>6}")
On a live IPL match, you’ll see 40–50 bookmakers pricing the match winner — including Pinnacle (the market benchmark), the Betfair Exchange, FanDuel, DraftKings, Dafabet, Betway, and dozens of regional books.
Step 4: Compare Sharp vs. Soft Lines
This is where it gets useful. Pinnacle sets the sharpest cricket line in the world. Comparing it against soft bookmakers is the foundation of every value betting and arbitrage strategy.
# Build a comparison table: sharp vs soft
SHARP_BOOKS = ["pinnacle"]
SOFT_BOOKS = ["fanduel", "draftkings", "betway", "dafabet", "paddypower"]
def get_match_winner_prices(odds_data, market_id="271"):
"""Extract match-winner prices for all bookmakers."""
prices = {}
for slug, bdata in odds_data.get("bookmakerOdds", {}).items():
markets = bdata.get("markets", {})
if market_id in markets:
outcomes = markets[market_id]["outcomes"]
home_price = outcomes.get("271", {}).get("players", {}).get("0", {}).get("price")
away_price = outcomes.get("272", {}).get("players", {}).get("0", {}).get("price")
if home_price and away_price:
prices[slug] = {"home": home_price, "away": away_price}
return prices
prices = get_match_winner_prices(odds_data)
print(f"\n{'Bookmaker':20s} {'Home':>8} {'Away':>8} {'Edge vs Pinnacle':>16}")
print("-" * 60)
pinnacle = prices.get("pinnacle")
if pinnacle:
# Pinnacle implied probabilities (no-vig midpoint)
pin_home_prob = 1 / pinnacle["home"]
pin_away_prob = 1 / pinnacle["away"]
overround = pin_home_prob + pin_away_prob
fair_home = pin_home_prob / overround
fair_away = pin_away_prob / overround
for slug in SHARP_BOOKS + SOFT_BOOKS:
if slug in prices:
p = prices[slug]
# Edge = (price * fair_prob) - 1
home_edge = (p["home"] * fair_home - 1) * 100
away_edge = (p["away"] * fair_away - 1) * 100
best_edge = max(home_edge, away_edge)
print(f" {slug:20s} {p['home']:>8.3f} {p['away']:>8.3f} {best_edge:>+14.1f}%")
When a soft bookmaker is slow to move and Pinnacle has already shifted, the edge column lights up. That’s the signal arb scanners and value bettors are looking for.
Step 5: Cricket-Specific Markets (Overs, Runs, Powerplay)
Cricket isn’t just match-winner. OddsPapi covers 175+ markets per IPL fixture, including:
| Market | Example ID | Description |
|---|---|---|
| Winner (incl. super over) | 271 | Match winner — the main line |
| 1X2 | 273 | Home / Draw / Away (Test matches) |
| Tied Match | 274 | Will the match end in a tie? |
| To Win the Coin Toss | 278 | Coin toss winner |
| Over/Under Runs 1st Innings | 27188+ | Total runs lines (multiple thresholds) |
| 3 Overs 1st Innings | 271832+ | Powerplay segment runs |
| 6 Overs 1st Innings | 273620+ | Full powerplay lines |
| 10 Overs 1st Innings | 278092+ | Mid-innings lines |
Don’t hardcode these — the catalog is too large and changes across formats. Query the market catalog and build a lookup:
# Build a market name lookup for cricket
time.sleep(0.2)
response = requests.get(f"{BASE_URL}/markets", params={
"apiKey": API_KEY,
"sportId": 27
})
catalog = response.json()
market_names = {m["marketId"]: m["marketName"] for m in catalog}
outcome_names = {
(m["marketId"], o["outcomeId"]): o["outcomeName"]
for m in catalog for o in m.get("outcomes", [])
}
print(f"Cricket market catalog: {len(catalog)} entries")
print(f"Unique market types: {len(set(market_names.values()))}")
# Show what markets a fixture actually has
for slug in ["pinnacle"]:
if slug in odds_data.get("bookmakerOdds", {}):
for mid in odds_data["bookmakerOdds"][slug]["markets"]:
name = market_names.get(int(mid), mid)
print(f" {mid}: {name}")
Step 6: Betfair Exchange — Full Depth of Book
If you’ve ever used the Betfair API, you know the pain: account verification, API keys, rate limits, session tokens. OddsPapi normalizes Betfair Exchange data into the same format as every other bookmaker — with the addition of back/lay depth.
# Betfair Exchange data comes with full order book depth
betfair = odds_data.get("bookmakerOdds", {}).get("betfair-ex", {})
if betfair:
for mid, mdata in betfair.get("markets", {}).items():
name = market_names.get(int(mid), mid)
print(f"\nMarket: {name} (ID: {mid})")
for oid, odata in mdata.get("outcomes", {}).items():
p0 = odata.get("players", {}).get("0", {})
price = p0.get("price")
meta = p0.get("exchangeMeta", {})
oname = outcome_names.get((int(mid), int(oid)), oid)
print(f" {oname}:")
print(f" Back: {price} (available: {p0.get('limit', 0):.2f})")
if meta and "availableToBack" in meta:
for level in meta["availableToBack"][:3]:
print(f" {level['price']} x £{level['size']:.2f}")
for level in meta.get("availableToLay", [])[:3]:
print(f" Lay {level['price']} x £{level['size']:.2f}")
This gives you the full Betfair order book — back prices, lay prices, and available liquidity at each level — without touching the Betfair API directly.
Step 7: Historical Odds for Backtesting
Building a cricket prediction model? You need historical closing lines to measure accuracy. Most providers charge for this. OddsPapi includes it on the free tier.
# Pull historical odds for a completed fixture
# Note: historical endpoint uses "bookmakers" key (not "bookmakerOdds")
# Max 3 bookmakers per call
completed = [f for f in fixtures if not f.get("hasOdds") or f["statusId"] != 1]
# Or use a known completed fixture ID
HISTORICAL_FIXTURE = completed[0]["fixtureId"] if completed else fid
time.sleep(0.2)
hist_response = requests.get(f"{BASE_URL}/historical-odds", params={
"apiKey": API_KEY,
"fixtureId": HISTORICAL_FIXTURE,
"bookmakers": "pinnacle,betfair-ex,dafabet"
})
hist_data = hist_response.json()
# The structure is different from live odds:
# hist_data["bookmakers"][slug]["markets"][mid]["outcomes"][oid]["players"]["0"]
# is a LIST of snapshots, not a single dict
for slug, bdata in hist_data.get("bookmakers", {}).items():
for mid, mdata in bdata.get("markets", {}).items():
for oid, odata in mdata.get("outcomes", {}).items():
snapshots = odata.get("players", {}).get("0", [])
if snapshots:
print(f"\n{slug} | Market {mid} | Outcome {oid}")
print(f" {len(snapshots)} price snapshots")
print(f" Opening: {snapshots[0]['price']} at {snapshots[0]['createdAt']}")
print(f" Closing: {snapshots[-1]['price']} at {snapshots[-1]['createdAt']}")
Key difference: The live /odds endpoint returns bookmakerOdds with a single price per outcome. The /historical-odds endpoint returns bookmakers with a list of timestamped price snapshots. Don’t mix up the response shapes.
Step 8: Full Pipeline — IPL Odds Scanner
Here’s a complete script that scans all upcoming IPL fixtures and finds where soft bookmakers are offering better prices than Pinnacle’s fair line:
import requests
import time
from datetime import datetime, timedelta
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def get_fixtures(sport_id, days=10):
"""Fetch fixtures with odds for a date range."""
today = datetime.utcnow().strftime("%Y-%m-%d")
end = (datetime.utcnow() + timedelta(days=days)).strftime("%Y-%m-%d")
r = requests.get(f"{BASE_URL}/fixtures", params={
"apiKey": API_KEY, "sportId": sport_id, "from": today, "to": end
})
return [f for f in r.json() if f.get("hasOdds")]
def get_odds(fixture_id):
"""Fetch live odds for a fixture."""
r = requests.get(f"{BASE_URL}/odds", params={
"apiKey": API_KEY, "fixtureId": fixture_id
})
return r.json()
def find_value(odds_data, market_id="271"):
"""Compare all bookmakers against Pinnacle's fair line."""
pin = odds_data.get("bookmakerOdds", {}).get("pinnacle", {})
pin_markets = pin.get("markets", {})
if market_id not in pin_markets:
return []
# Calculate Pinnacle fair probabilities
outcomes = pin_markets[market_id]["outcomes"]
pin_prices = {}
for oid, odata in outcomes.items():
p = odata.get("players", {}).get("0", {}).get("price")
if p:
pin_prices[oid] = p
if len(pin_prices) < 2:
return []
overround = sum(1/p for p in pin_prices.values())
fair_probs = {oid: (1/p) / overround for oid, p in pin_prices.items()}
# Scan all bookmakers
edges = []
for slug, bdata in odds_data.get("bookmakerOdds", {}).items():
if slug == "pinnacle":
continue
markets = bdata.get("markets", {})
if market_id not in markets:
continue
for oid, odata in markets[market_id].get("outcomes", {}).items():
price = odata.get("players", {}).get("0", {}).get("price")
if price and oid in fair_probs:
edge = (price * fair_probs[oid] - 1) * 100
if edge > 0:
edges.append({
"bookmaker": slug,
"outcome": oid,
"price": price,
"pinnacle_price": pin_prices[oid],
"edge": edge
})
return sorted(edges, key=lambda x: -x["edge"])
# Run the scanner
fixtures = get_fixtures(sport_id=27)
ipl = [f for f in fixtures if "Indian Premier League" in f.get("tournamentName", "")]
print(f"Scanning {len(ipl)} IPL fixtures...\n")
for fixture in ipl:
name = f"{fixture['participant1Name']} vs {fixture['participant2Name']}"
time.sleep(0.2)
odds = get_odds(fixture["fixtureId"])
values = find_value(odds)
if values:
print(f"\n{name}")
for v in values[:5]:
side = "Home" if v["outcome"] == "271" else "Away"
print(f" {v['bookmaker']:20s} {side} {v['price']:.3f} "
f"(Pin: {v['pinnacle_price']:.3f}) Edge: {v['edge']:+.1f}%")
Coverage: What Tournaments Are Included?
| Tournament | Format | Typical Bookmakers |
|---|---|---|
| Indian Premier League (IPL) | T20 | 49+ |
| Pakistan Super League (PSL) | T20 | 30+ |
| Big Bash League | T20 | 20+ |
| Caribbean Premier League | T20 | 20+ |
| International T20s | T20 | 20+ |
| ODI Series | ODI | 20+ |
| West Indies Championship | First-class | 10+ |
| County Cricket / Domestic | Various | 10+ |
Coverage scales with liquidity. The IPL gets the deepest bookmaker coverage because it has the most betting volume. Niche domestic leagues still get odds, just from fewer books.
Why Not Just Use The Odds API or CricketAPI?
The Odds API covers cricket, but with ~40 bookmakers total across all sports. You won’t find Dafabet, TabTouch, Pamestoixima, or the other regional books that price cricket aggressively. And you won’t get Betfair Exchange depth.
CricketAPI (Roanuz) and EntitySport are excellent for scores, ball-by-ball data, and player stats. They don’t provide bookmaker odds. You’d need both CricketAPI and OddsPapi to build a complete cricket data pipeline — stats from one, prices from the other.
Enterprise feeds (Sportradar, LSports, OddsMatrix) cover cricket with depth, but the free tier either doesn’t exist or is too restrictive for development work. OddsPapi gives you free historical data, WebSocket streaming, and 350+ bookmakers across all sports — cricket is one of 69 supported sports.
Quick Reference: Cricket API Endpoints
| Endpoint | Purpose | Key Parameters |
|---|---|---|
GET /fixtures |
List cricket matches | sportId=27, from, to |
GET /odds |
Live odds for a fixture | fixtureId |
GET /historical-odds |
Price history for backtesting | fixtureId, bookmakers (max 3) |
GET /markets |
Market catalog & outcome names | sportId=27 |
GET /bookmakers |
Full bookmaker list | — |
All endpoints use ?apiKey=YOUR_KEY as a query parameter. Rate limit on free tier: ~1 request per second per endpoint. Add time.sleep(0.2) between iterations.
Get Started
Stop scraping Cricbuzz for odds that update every 30 seconds. Get your free API key, point your script at sportId=27, and start pulling real-time cricket lines from Pinnacle, Betfair, and 47 other bookmakers in under five minutes.
The IPL is live right now. Your model should be too.