The Australian Open 2026 starts in just a few days, and bettors who line shop across sportsbooks consistently get better odds than those who stick to a single book. Different sportsbooks price tennis matches differently, and finding the best available line can significantly improve your expected value over time.
This tutorial shows you how to use the BALLDONTLIE API to compare ATP tennis odds across multiple sportsbooks programmatically. We'll focus on 1st round main draw matches (Round of 128) where you can find odds for marquee players like Carlos Alcaraz and Alexander Zverev.
This tutorial assumes you have an API key. If you haven't set that up yet, see our Getting Started guide.
What is Line Shopping in Tennis?
Tennis betting is inherently simpler than team sports - there are only two outcomes (ignoring retirement scenarios). But sportsbooks still disagree on pricing. For an Australian Open match, you might see:
- FanDuel: Sinner -250, Djokovic +200
- DraftKings: Sinner -230, Djokovic +190
- Caesars: Sinner -240, Djokovic +195
Betting Sinner at DraftKings (-230) is better than at FanDuel (-250) for the same outcome. Over hundreds of bets, these differences add up.
Fetching Australian Open Odds
Let's start by finding the Australian Open tournament and fetching odds for upcoming matches:
import requests
API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"
headers = {"Authorization": API_KEY}
# Find the Australian Open tournament
response = requests.get(
f"{BASE_URL}/atp/v1/tournaments",
headers=headers,
params={"season": 2026, "name": "Australian Open"}
)
tournaments = response.json()["data"]
if tournaments:
tournament = tournaments[0]
print(f"Found: {tournament['name']}")
print(f"Location: {tournament.get('location')}")
print(f"Category: {tournament.get('category')}")
print(f"Surface: {tournament.get('surface')}")
The API returns tournament details including the category (Grand Slam, ATP 1000, etc.) and surface type. The Australian Open is played on hard courts in Melbourne.
Getting Match Odds
Now let's fetch odds for 1st round main draw matches (Round of 128 in Grand Slams) in the tournament:
import requests
from collections import defaultdict
API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"
headers = {"Authorization": API_KEY}
tournament_id = 225 # Australian Open ID
# Fetch odds for the tournament
response = requests.get(
f"{BASE_URL}/atp/v1/odds",
headers=headers,
params={"tournament_ids[]": tournament_id, "season": 2026}
)
odds_data = response.json()["data"]
print(f"Found {len(odds_data)} odds entries")
# Group odds by match
match_odds = defaultdict(list)
for odd in odds_data:
match_odds[odd["match_id"]].append(odd)
print(f"Odds available for {len(match_odds)} matches")
The API returns odds from the following sportsbooks:
- FanDuel
- DraftKings
- Caesars
Each odds entry includes moneyline odds for both players in American format.
Comparing Odds Across Sportsbooks
Here's a script that finds the best available odds for each player in a match:
import requests
from collections import defaultdict
API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"
headers = {"Authorization": API_KEY}
tournament_id = 225 # Australian Open ID
# Fetch 1st round matches (Round of 128 for Grand Slams)
matches_resp = requests.get(
f"{BASE_URL}/atp/v1/matches",
headers=headers,
params={"tournament_ids[]": tournament_id, "season": 2026, "round": "Round of 128"}
)
matches = {m["id"]: m for m in matches_resp.json()["data"]}
# Fetch odds
odds_resp = requests.get(
f"{BASE_URL}/atp/v1/odds",
headers=headers,
params={"tournament_ids[]": tournament_id, "season": 2026}
)
# Group odds by match
match_odds = defaultdict(list)
for odd in odds_resp.json()["data"]:
match_odds[odd["match_id"]].append(odd)
def find_best_odds(odds_list, player_key):
"""Find the best (highest) odds for a player."""
valid = [o for o in odds_list if o[player_key] is not None]
if not valid:
return None, None
best = max(valid, key=lambda x: x[player_key])
return best["vendor"], best[player_key]
# Display best odds for each match
for match_id, odds_list in match_odds.items():
if match_id not in matches:
continue
match = matches[match_id]
player1 = match.get("player1", {})
player2 = match.get("player2", {})
p1_name = f"{player1.get('first_name', '')} {player1.get('last_name', '')}"
p2_name = f"{player2.get('first_name', '')} {player2.get('last_name', '')}"
round_name = match.get("round", "TBD")
print(f"\n{round_name}: {p1_name} vs {p2_name}")
print("-" * 50)
p1_vendor, p1_odds = find_best_odds(odds_list, "player1_odds")
p2_vendor, p2_odds = find_best_odds(odds_list, "player2_odds")
if p1_odds:
print(f"{p1_name}: {p1_odds:+d} at {p1_vendor.capitalize()}")
if p2_odds:
print(f"{p2_name}: {p2_odds:+d} at {p2_vendor.capitalize()}")
Example Output:
Round of 128: Alexander Zverev vs Gabriel Diallo
--------------------------------------------------
Alexander Zverev: -660 at Draftkings
Gabriel Diallo: +580 at Fanduel
Round of 128: Carlos Alcaraz vs Adam Walton
--------------------------------------------------
Carlos Alcaraz: -6300 at Draftkings
Adam Walton: +2800 at Fanduel
Round of 128: Francisco Cerundolo vs Zhizhen Zhang
--------------------------------------------------
Francisco Cerundolo: -240 at Caesars
Zhizhen Zhang: +220 at Fanduel
Converting Odds to Implied Probability
American odds represent the sportsbook's implied probability for each outcome. Converting these helps you understand the true market pricing:
def american_to_implied_prob(odds):
"""Convert American odds to implied probability."""
if odds > 0:
return 100 / (odds + 100)
else:
return abs(odds) / (abs(odds) + 100)
# Examples
print(f"-250 implies: {american_to_implied_prob(-250):.1%}") # 71.4%
print(f"+200 implies: {american_to_implied_prob(200):.1%}") # 33.3%
print(f"-150 implies: {american_to_implied_prob(-150):.1%}") # 60.0%
print(f"+130 implies: {american_to_implied_prob(130):.1%}") # 43.5%
Complete Odds Analysis Script
Here's a full script that analyzes Australian Open matches and highlights the best available odds:
import requests
from collections import defaultdict
API_KEY = "your-api-key"
BASE_URL = "https://api.balldontlie.io"
headers = {"Authorization": API_KEY}
def american_to_implied_prob(odds):
if odds > 0:
return 100 / (odds + 100)
else:
return abs(odds) / (abs(odds) + 100)
def find_best_odds(odds_list, player_key):
valid = [o for o in odds_list if o[player_key] is not None]
if not valid:
return None, None
best = max(valid, key=lambda x: x[player_key])
return best["vendor"], best[player_key]
# Fetch tournament
tournaments_resp = requests.get(
f"{BASE_URL}/atp/v1/tournaments",
headers=headers,
params={"season": 2026, "name": "Australian Open"}
)
tournament = tournaments_resp.json()["data"][0]
tournament_id = tournament["id"]
print(f"AUSTRALIAN OPEN 2026 - ODDS ANALYSIS")
print(f"Location: {tournament.get('location', 'Melbourne, Australia')}")
print(f"Surface: {tournament.get('surface', 'Hard')}")
print("=" * 70)
# Fetch 1st round matches and odds
matches_resp = requests.get(
f"{BASE_URL}/atp/v1/matches",
headers=headers,
params={"tournament_ids[]": tournament_id, "season": 2026, "round": "Round of 128"}
)
matches = {m["id"]: m for m in matches_resp.json()["data"]}
odds_resp = requests.get(
f"{BASE_URL}/atp/v1/odds",
headers=headers,
params={"tournament_ids[]": tournament_id, "season": 2026}
)
match_odds = defaultdict(list)
for odd in odds_resp.json()["data"]:
match_odds[odd["match_id"]].append(odd)
# Analyze each match
for match_id, odds_list in match_odds.items():
if match_id not in matches:
continue
match = matches[match_id]
player1 = match.get("player1", {})
player2 = match.get("player2", {})
p1_name = f"{player1.get('first_name', '')} {player1.get('last_name', '')}"
p2_name = f"{player2.get('first_name', '')} {player2.get('last_name', '')}"
round_name = match.get("round", "TBD")
print(f"\n{round_name}")
print(f"{p1_name} vs {p2_name}")
print("-" * 60)
p1_vendor, p1_odds = find_best_odds(odds_list, "player1_odds")
p2_vendor, p2_odds = find_best_odds(odds_list, "player2_odds")
if p1_odds and p2_odds:
p1_prob = american_to_implied_prob(p1_odds)
p2_prob = american_to_implied_prob(p2_odds)
combined = p1_prob + p2_prob
print(f" {p1_name}: {p1_odds:+d} at {p1_vendor.capitalize()} ({p1_prob:.1%})")
print(f" {p2_name}: {p2_odds:+d} at {p2_vendor.capitalize()} ({p2_prob:.1%})")
print(f"\n Combined implied probability: {combined:.1%}")
if combined < 1.0:
edge = (1 - combined) * 100
print(f" >>> Potential value: {edge:.1f}% theoretical edge")
# Show all vendor odds
print("\n All sportsbooks:")
for odd in odds_list:
vendor = odd["vendor"]
o1 = odd.get("player1_odds")
o2 = odd.get("player2_odds")
if o1 and o2:
print(f" {vendor.capitalize():12} | {o1:+6d} | {o2:+6d}")
Example Output:
AUSTRALIAN OPEN 2026 - ODDS ANALYSIS
Location: Melbourne, Australia
Surface: Hard
======================================================================
Round of 128
Carlos Alcaraz vs Adam Walton
------------------------------------------------------------
Carlos Alcaraz: -6300 at Draftkings (98.4%)
Adam Walton: +2800 at Fanduel (3.4%)
Combined implied probability: 101.9%
All sportsbooks:
Caesars | -10000 | +1800
Draftkings | -6300 | +2700
Fanduel | -10000 | +2800
When the combined implied probability is less than 100%, it means the best odds across books are favorable. This is rare but worth monitoring.
Key Tennis Betting Insights
The ATP odds endpoint provides several useful data points:
- Match odds by round: Filter odds by tournament round to focus on specific stages
- Player filtering: Get odds for matches involving specific players
- Multi-vendor comparison: See how different books price the same match
Tennis odds tend to vary more for:
- Early round matches with lesser-known players
- Matches between similarly-ranked opponents
- Markets where one book has stronger opinions
Important Limitations
The BALLDONTLIE API provides current odds only. We do not store historical betting odds.
This data is ideal for:
- Live line shopping: Finding the best current odds before placing a bet
- Real-time monitoring: Building alerts when lines move
- Market analysis: Understanding current market pricing
This data is not suitable for:
- Historical backtesting of betting strategies
- Analyzing past line movements
- Training ML models on historical odds
Other ATP Data Available
Beyond betting odds, the BALLDONTLIE API provides comprehensive ATP tennis data:
- Players: Search players by name, country, or ranking range
- Rankings: Current ATP rankings with points and movement
- Tournaments: All ATP events including Grand Slams, Masters 1000, and more
- Matches: Match results with scores and round information
- Head-to-Head: Historical matchup records between players
- Match Statistics: Detailed serve and return statistics (aces, break points, etc.)
- Race to Turin: ATP Finals qualification standings
Extending This Further
Want to build on this script? Check out our AI-assisted workflow to quickly generate variations. You could:
- Add notifications when odds move beyond a threshold
- Compare odds across multiple tournaments simultaneously
- Build a Discord bot that alerts you to favorable lines
- Track specific players across their tournament draws
Other Sports Data
The BALLDONTLIE API covers more than just tennis. We provide data for:
- NBA, WNBA: Games, player stats, betting odds, player props
- NFL, NCAAF, NCAAB: Games, player stats, team stats, betting odds
- MLB: Games, player stats, team stats
- NHL: Games, player stats, standings, betting odds
- EPL, La Liga, Serie A, Bundesliga, Ligue 1, MLS, Champions League: Soccer/football data
- MMA: UFC events, fights, fighter stats, betting odds
- PGA Tour: Golf tournaments and leaderboards
Check the full API documentation for complete endpoint details across all leagues.
Next Steps
Ready to start building? Here are some resources:
- Get your API key: Sign up at app.balldontlie.io
- Speed up development: Let AI write your integration code
- NBA odds comparison: Compare odds across sportsbooks for NBA games
Need help? Join our Discord community or check the documentation.