How to Build a Betting App with Python (Beginner Guide)
Every Betting API Tutorial Assumes You Already Know What You’re Doing
Search “sports betting API tutorial” and you’ll find docs that jump straight into fixtures, markets, and nested JSON payloads — assuming you already know what those words mean. If you’re a developer who wants to build something with odds data but hasn’t worked with betting APIs before, you’re left reading three different docs before writing a single line of code.
This tutorial is different. We start from zero. By the end, you’ll have a working Python script that pulls live odds from multiple bookmakers and shows you which one has the best price — a real odds comparison tool, not a toy.
Most sports betting APIs either lock you behind enterprise contracts or only cover 20–40 “soft” bookmakers (the retail sportsbooks). OddsPapi gives you 350+ bookmakers — including sharps like Pinnacle and SBOBET, plus crypto books and exchanges — on a free tier. That means you can build real tools without spending anything.
What You’ll Build
A command-line odds comparison tool that:
- Lists available sports from the API
- Fetches upcoming fixtures (matches) for a sport you pick
- Pulls live odds from three bookmakers — Pinnacle, Bet365, and DraftKings
- Displays a side-by-side comparison table
- Highlights the best price for each outcome
Here’s what the final output looks like:
Available Sports:
1. Soccer (ID: 10)
2. Basketball (ID: 11)
3. Tennis (ID: 12)
...
Pick a sport number: 1
Upcoming Soccer Fixtures:
1. Chelsea FC vs Manchester City (Premier League)
2. Bologna FC vs US Lecce (Serie A)
...
Pick a fixture number: 1
Odds Comparison: Chelsea FC vs Manchester City
Full Time Result (1X2)
┌──────────┬──────────┬──────────┬────────────┐
│ Outcome │ Pinnacle │ Bet365 │ DraftKings │
├──────────┼──────────┼──────────┼────────────┤
│ Home │ 3.79 │ 3.60 │ 3.60 │
│ Draw │ 3.03 │ 3.90 │ 3.00 │
│ Away │ 2.15 │ 1.90 │ 2.20 │
└──────────┴──────────┴──────────┴────────────┘
Best Home: Pinnacle @ 3.79 | Best Draw: Bet365 @ 3.90 | Best Away: DraftKings @ 2.20
Every step teaches a core API concept. By the end, you’ll understand the full data model and be ready to build more advanced tools.
Prerequisites
You need:
- Python 3.8+ (check with
python3 --version) - The
requestslibrary:pip install requests - A free OddsPapi API key — sign up at oddspapi.io (no credit card required)
- Basic Python knowledge — variables, loops, functions, dictionaries
That’s it. No frameworks, no databases, no frontend. Just Python and HTTP requests.
The Data Model: How Betting Data Is Organized
Before writing code, let’s understand how the API structures its data. Every sports betting API follows roughly the same hierarchy:
Sport (Soccer, Basketball, Tennis...)
└─ Tournament (Premier League, NBA, ATP...)
└─ Fixture (Chelsea vs Man City, Lakers vs Celtics...)
└─ Market (Full Time Result, Over/Under 2.5, Asian Handicap...)
└─ Outcome (Home, Draw, Away, Over, Under...)
└─ Odds (the actual price from each bookmaker)
If you’ve used other APIs, the terminology might be different from what you’re used to. Here’s the mapping:
| What You Might Call It | OddsPapi Calls It | Example |
|---|---|---|
| League (NFL, Premier League) | Tournament | English Premier League |
| Game / Match | Fixture | Chelsea vs Manchester City |
| Team | Participant | Chelsea FC |
| Bet Type (moneyline, spread) | Market | Full Time Result |
| Selection (Home, Away, Over) | Outcome | Home (1) |
Keep this table handy. The rest of the tutorial uses OddsPapi terminology.
Step 1: Authentication & Your First API Call
OddsPapi uses a query parameter for authentication — not a header. This is the most common mistake beginners make, so let’s get it right from the start:
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
# Fetch all available sports
response = requests.get(
f"{BASE_URL}/sports",
params={"apiKey": API_KEY}
)
sports = response.json()
print(f"Found {len(sports)} sports:\n")
for sport in sports[:10]:
print(f" {sport['sportName']} (ID: {sport['sportId']}, slug: {sport['slug']})")
The response is a list of objects, each with three fields:
sportId— the numeric ID you’ll use in other API callsslug— a URL-friendly name (e.g.,soccer,basketball)sportName— the human-readable name
Found 69 sports:
Soccer (ID: 10, slug: soccer)
Basketball (ID: 11, slug: basketball)
Tennis (ID: 12, slug: tennis)
Baseball (ID: 13, slug: baseball)
American Football (ID: 14, slug: american-football)
Ice Hockey (ID: 15, slug: ice-hockey)
ESport Dota (ID: 16, slug: esport-dota)
ESport Counter-Strike (ID: 17, slug: esport-counter-strike)
ESport League of Legends (ID: 18, slug: esport-league-of-legends)
MMA (ID: 20, slug: mma)
Important: The apiKey goes in the query string (params=), not in the headers. If you put it in an Authorization header, you’ll get a 401 error.
Step 2: Find Upcoming Fixtures
Now let’s fetch some actual matches. The /fixtures endpoint requires a sport ID and a date range:
from datetime import datetime, timedelta, timezone
# Fetch soccer fixtures for the next 3 days
now = datetime.now(timezone.utc)
date_from = now.strftime("%Y-%m-%dT00:00:00Z")
date_to = (now + timedelta(days=3)).strftime("%Y-%m-%dT23:59:59Z")
response = requests.get(
f"{BASE_URL}/fixtures",
params={
"apiKey": API_KEY,
"sportId": 10, # Soccer
"from": date_from,
"to": date_to
}
)
fixtures = response.json()
print(f"Total fixtures: {len(fixtures)}")
# Filter to only fixtures that have odds available
fixtures_with_odds = [f for f in fixtures if f.get("hasOdds")]
print(f"Fixtures with odds: {len(fixtures_with_odds)}\n")
# Display the first 10
for i, fix in enumerate(fixtures_with_odds[:10], 1):
home = fix["participant1Name"]
away = fix["participant2Name"]
tournament = fix["tournamentName"]
start = fix["startTime"][:16].replace("T", " ")
print(f" {i}. {home} vs {away}")
print(f" {tournament} — {start} UTC")
A few things to note:
- The
fromandtoparameters use ISO 8601 format. The maximum range is 10 days. hasOdds: truemeans at least one bookmaker is pricing this fixture. Always filter on this — fixtures without odds won’t return useful data from the odds endpoint.- Each fixture has a
fixtureId(likeid1000001761301147) that you’ll use to fetch odds. - Teams are called
participant1Name(home) andparticipant2Name(away).
Step 3: Fetch Live Odds
This is where it gets interesting. The /odds endpoint returns live prices from the bookmakers you specify:
import time
# Pick a fixture (use the fixtureId from Step 2)
fixture_id = fixtures_with_odds[0]["fixtureId"]
home_team = fixtures_with_odds[0]["participant1Name"]
away_team = fixtures_with_odds[0]["participant2Name"]
time.sleep(0.2) # Respect the rate limit
response = requests.get(
f"{BASE_URL}/odds",
params={
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmakers": "pinnacle,bet365,draftkings"
}
)
odds_data = response.json()
The response JSON is deeply nested. This is where most beginners get stuck, so let’s walk through it layer by layer:
# The path to a single price:
# odds_data["bookmakerOdds"][bookmaker_slug]["markets"][market_id]["outcomes"][outcome_id]["players"]["0"]["price"]
# Let's trace it step by step:
# 1. Get the bookmaker odds container
bookmaker_odds = odds_data["bookmakerOdds"]
print(f"Bookmakers: {list(bookmaker_odds.keys())}")
# → ['bet365', 'pinnacle', 'draftkings']
# 2. Pick a bookmaker
pinnacle = bookmaker_odds["pinnacle"]
# 3. Get the markets dict (keys are market IDs as strings)
markets = pinnacle["markets"]
print(f"Market IDs available: {list(markets.keys())[:5]}...")
# → ['101', '104', '106', '108', '1010'...]
# 4. Pick market 101 (Full Time Result / 1X2)
market_101 = markets["101"]
# 5. Get outcomes (keys are outcome IDs as strings)
outcomes = market_101["outcomes"]
print(f"Outcome IDs: {list(outcomes.keys())}")
# → ['101', '102', '103']
# 6. Get the price for outcome 101 (Home win)
home_odds = outcomes["101"]["players"]["0"]["price"]
print(f"Pinnacle Home price: {home_odds}")
# → 3.79
Why players["0"]? For standard markets, "0" represents the current price. Player prop markets use player IDs as keys instead — but for this tutorial, "0" is all you need.
What Do the Market and Outcome IDs Mean?
Market ID 101 and outcome ID 101 are just numbers — you need to look them up. The /markets endpoint gives you human-readable names:
time.sleep(0.2)
# Fetch the market catalog for soccer
response = requests.get(
f"{BASE_URL}/markets",
params={"apiKey": API_KEY, "sportId": 10}
)
catalog = response.json()
# Build lookup dicts
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", [])
}
# Now you can translate IDs to names
print(market_names[101]) # → "Full Time Result"
print(outcome_names[(101, 101)]) # → "1" (Home)
print(outcome_names[(101, 102)]) # → "X" (Draw)
print(outcome_names[(101, 103)]) # → "2" (Away)
For the Full Time Result market, the outcome names are shorthand: 1 = Home win, X = Draw, 2 = Away win. This is standard 1X2 notation used across the industry.
Step 4: Build the Comparison Table
Now let’s pull it all together and compare prices across bookmakers:
def compare_odds(odds_data, market_id, market_name, outcome_ids, outcome_labels):
"""Compare prices across bookmakers for a given market."""
bookmaker_odds = odds_data.get("bookmakerOdds", {})
slugs = list(bookmaker_odds.keys())
if not slugs:
print("No bookmaker odds available for this fixture.")
return
# Header
print(f"\n{market_name}")
header = f"{'Outcome':<10}"
for slug in slugs:
header += f" {slug:<12}"
header += " Best"
print(header)
print("-" * len(header))
# Each outcome row
for oid, label in zip(outcome_ids, outcome_labels):
row = f"{label:<10}"
best_price = 0
best_book = ""
for slug in slugs:
try:
price = bookmaker_odds[slug]["markets"][str(market_id)]["outcomes"][str(oid)]["players"]["0"]["price"]
row += f" {price:<12.3f}"
if price > best_price:
best_price = price
best_book = slug
except KeyError:
row += f" {'—':<12}"
row += f" {best_book} @ {best_price:.3f}"
print(row)
# Use it
compare_odds(
odds_data,
market_id=101,
market_name="Full Time Result (1X2)",
outcome_ids=[101, 102, 103],
outcome_labels=["Home", "Draw", "Away"]
)
Output:
Full Time Result (1X2)
Outcome bet365 pinnacle draftkings Best
---------------------------------------------------------------
Home 3.600 3.790 3.600 pinnacle @ 3.790
Draw 3.900 3.030 3.000 bet365 @ 3.900
Away 1.900 2.150 2.200 draftkings @ 2.200
The "Best" column shows which bookmaker offers the highest price for each outcome. This is called line shopping — finding the best available price before placing a bet. Even small differences (like 3.79 vs 3.60 for a home win) add up over hundreds of bets.
Step 5: The Complete Script
Here's the full working script that ties everything together into an interactive tool:
"""
Odds Comparison Tool — Built with OddsPapi API
Compare live prices across bookmakers for any sport and fixture.
"""
import requests
import time
from datetime import datetime, timedelta, timezone
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
BOOKMAKERS = "pinnacle,bet365,draftkings"
def api_get(endpoint, **params):
"""Make an authenticated GET request to the OddsPapi API."""
params["apiKey"] = API_KEY
response = requests.get(f"{BASE_URL}{endpoint}", params=params)
response.raise_for_status()
time.sleep(0.2) # Respect rate limits
return response.json()
def pick_sport():
"""Display sports and let the user choose one."""
sports = api_get("/sports")
print("\nAvailable Sports:")
for i, sport in enumerate(sports, 1):
print(f" {i}. {sport['sportName']} (ID: {sport['sportId']})")
choice = int(input("\nPick a sport number: ")) - 1
return sports[choice]
def pick_fixture(sport_id):
"""Fetch fixtures and let the user choose one."""
now = datetime.now(timezone.utc)
fixtures = api_get(
"/fixtures",
sportId=sport_id,
**{"from": now.strftime("%Y-%m-%dT00:00:00Z")},
to=(now + timedelta(days=3)).strftime("%Y-%m-%dT23:59:59Z")
)
with_odds = [f for f in fixtures if f.get("hasOdds")]
if not with_odds:
print("No fixtures with odds found. Try a different sport.")
return None
print(f"\nUpcoming Fixtures ({len(with_odds)} with odds):")
for i, fix in enumerate(with_odds[:15], 1):
start = fix["startTime"][:16].replace("T", " ")
print(f" {i}. {fix['participant1Name']} vs {fix['participant2Name']}")
print(f" {fix['tournamentName']} — {start} UTC")
choice = int(input("\nPick a fixture number: ")) - 1
return with_odds[choice]
def get_market_names(sport_id):
"""Build lookup dicts for market and outcome names."""
catalog = api_get("/markets", sportId=sport_id)
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", [])
}
return market_names, outcome_names
def display_comparison(odds_data, market_id, outcome_ids, market_names, outcome_names):
"""Print a comparison table for one market across all bookmakers."""
bookmaker_odds = odds_data.get("bookmakerOdds", {})
slugs = list(bookmaker_odds.keys())
if not slugs:
print("No bookmaker odds available.")
return
mkt_name = market_names.get(market_id, f"Market {market_id}")
print(f"\n{mkt_name}")
header = f" {'Outcome':<10}"
for slug in slugs:
header += f" {slug:<12}"
header += " Best Price"
print(header)
print(" " + "-" * (len(header) - 2))
for oid in outcome_ids:
label = outcome_names.get((market_id, oid), str(oid))
row = f" {label:<10}"
best_price = 0
best_book = ""
for slug in slugs:
try:
price = (bookmaker_odds[slug]["markets"][str(market_id)]
["outcomes"][str(oid)]["players"]["0"]["price"])
row += f" {price:<12.3f}"
if price > best_price:
best_price = price
best_book = slug
except KeyError:
row += f" {'—':<12}"
row += f" {best_book} @ {best_price:.2f}"
print(row)
def main():
print("=" * 50)
print(" OddsPapi Odds Comparison Tool")
print("=" * 50)
# Step 1: Pick a sport
sport = pick_sport()
sport_id = sport["sportId"]
print(f"\nSelected: {sport['sportName']}")
# Step 2: Pick a fixture
fixture = pick_fixture(sport_id)
if not fixture:
return
fixture_id = fixture["fixtureId"]
home = fixture["participant1Name"]
away = fixture["participant2Name"]
print(f"\nSelected: {home} vs {away}")
# Step 3: Fetch odds and market names
print("\nFetching odds...")
odds_data = api_get("/odds", fixtureId=fixture_id, bookmakers=BOOKMAKERS)
market_names, outcome_names = get_market_names(sport_id)
if "bookmakerOdds" not in odds_data:
print("No odds available for this fixture yet.")
return
print(f"\nOdds Comparison: {home} vs {away}")
# Step 4: Display Full Time Result (market 101)
display_comparison(odds_data, 101, [101, 102, 103], market_names, outcome_names)
# Also show Over/Under 2.5 if available (market 1010)
first_slug = list(odds_data["bookmakerOdds"].keys())[0]
if "1010" in odds_data["bookmakerOdds"][first_slug].get("markets", {}):
display_comparison(odds_data, 1010, [1010, 1011], market_names, outcome_names)
if __name__ == "__main__":
main()
Bonus: Turn It Into a Web App with Streamlit
Want to share this tool with someone who doesn't use a terminal? Streamlit turns the same logic into a browser-based app with about 30 lines of extra code.
Install it:
pip install streamlit
Then create odds_app.py:
"""
Odds Comparison Web App — Built with OddsPapi + Streamlit
Run with: streamlit run odds_app.py
"""
import streamlit as st
import requests
import time
import pandas as pd
from datetime import datetime, timedelta, timezone
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
BOOKMAKERS = "pinnacle,bet365,draftkings"
def api_get(endpoint, **params):
params["apiKey"] = API_KEY
r = requests.get(f"{BASE_URL}{endpoint}", params=params)
r.raise_for_status()
time.sleep(0.2)
return r.json()
st.title("Odds Comparison Tool")
# Sport selector
sports = api_get("/sports")
sport_names = {s["sportName"]: s["sportId"] for s in sports}
selected_sport = st.selectbox("Sport", list(sport_names.keys()))
sport_id = sport_names[selected_sport]
# Fetch fixtures
now = datetime.now(timezone.utc)
fixtures = api_get(
"/fixtures", sportId=sport_id,
**{"from": now.strftime("%Y-%m-%dT00:00:00Z")},
to=(now + timedelta(days=3)).strftime("%Y-%m-%dT23:59:59Z")
)
with_odds = [f for f in fixtures if f.get("hasOdds")]
if not with_odds:
st.warning("No fixtures with odds available.")
st.stop()
# Fixture selector
fixture_labels = {
f"{f['participant1Name']} vs {f['participant2Name']} ({f['tournamentName']})": f
for f in with_odds[:20]
}
selected_fix = st.selectbox("Fixture", list(fixture_labels.keys()))
fixture = fixture_labels[selected_fix]
# Fetch and display odds
odds_data = api_get("/odds", fixtureId=fixture["fixtureId"], bookmakers=BOOKMAKERS)
if "bookmakerOdds" not in odds_data:
st.info("No odds available for this fixture yet.")
st.stop()
# Build comparison table for Full Time Result (market 101)
rows = []
labels = {101: "Home", 102: "Draw", 103: "Away"}
bookmaker_odds = odds_data["bookmakerOdds"]
for oid, label in labels.items():
row = {"Outcome": label}
for slug in bookmaker_odds:
try:
price = (bookmaker_odds[slug]["markets"]["101"]
["outcomes"][str(oid)]["players"]["0"]["price"])
row[slug] = price
except KeyError:
row[slug] = None
rows.append(row)
df = pd.DataFrame(rows).set_index("Outcome")
st.subheader("Full Time Result (1X2)")
st.dataframe(df.style.highlight_max(axis=1), use_container_width=True)
Run it with streamlit run odds_app.py and you'll have a live odds comparison dashboard in your browser. For a more advanced version with auto-refresh and multiple markets, check out our full Streamlit dashboard tutorial.
Understanding the Numbers
The API returns decimal odds (the standard in Europe and Asia). Here's what you need to know:
| Concept | Formula | Example (odds = 3.79) |
|---|---|---|
| Implied Probability | 1 / odds |
1 / 3.79 = 26.4% |
| Potential Payout | stake × odds |
$10 × 3.79 = $37.90 |
| Profit | stake × (odds - 1) |
$10 × 2.79 = $27.90 |
Why "best price" matters: If Pinnacle offers 3.79 and Bet365 offers 3.60 for the same outcome, Pinnacle is giving you better value. On a $100 bet, that's $379 vs $360 in potential payout — a $19 difference from the same bet. Across hundreds of bets, line shopping for the best price is one of the simplest edges you can find.
# Quick odds conversion helpers
def to_probability(decimal_odds):
"""Convert decimal odds to implied probability."""
return 1 / decimal_odds
def to_american(decimal_odds):
"""Convert decimal odds to American format."""
if decimal_odds >= 2.0:
return f"+{int((decimal_odds - 1) * 100)}"
else:
return f"-{int(100 / (decimal_odds - 1))}"
# Examples
print(to_probability(3.79)) # 0.2638 → 26.4%
print(to_american(3.79)) # +279
print(to_american(1.50)) # -200
Where to Go Next
You now know how to authenticate, fetch fixtures, parse nested odds JSON, and compare prices. That's the foundation for everything else. Here's what to build next, in order of complexity:
| Project | What You'll Learn | Tutorial |
|---|---|---|
| Live Odds Dashboard | Auto-refreshing Streamlit app with multiple markets | Build a Dashboard |
| Arbitrage Bot | Find guaranteed-profit opportunities across bookmakers | Build an Arb Bot |
| Value Betting Scanner | Identify bets where the bookmaker's price is too high | Value Scanner |
| Backtest a Model | Use free historical odds to test strategies on past data | Backtest Tutorial |
| Player Props | Parse player-level prop markets (NFL, NBA, MLB) | Player Props Guide |
OddsPapi also gives you free historical odds data on the free tier — most competitors charge extra for this. If you want to backtest a strategy before risking real money, the historical endpoint is where you start.
FAQ
Is the OddsPapi API free?
Yes. The free tier gives you 250 API requests and access to all 350+ bookmakers, including historical odds. No credit card required to sign up.
What programming language do I need?
This tutorial uses Python, but the API is just HTTP + JSON. Any language that can make web requests works — JavaScript, Go, Java, C#, whatever you prefer.
Do I need to know about sports betting?
No. This tutorial explains the core concepts as you go. By the end, you'll understand fixtures, markets, outcomes, and odds well enough to build real tools.
How many bookmakers can I compare?
Over 350, including sharp bookmakers (Pinnacle, SBOBET, Singbet), major sportsbooks (Bet365, DraftKings, FanDuel), crypto books (1xBet, Stake), and prediction markets (Polymarket). You can request any combination in a single API call.
What's the rate limit on the free tier?
Approximately 0.88 seconds between calls to the same endpoint. Adding time.sleep(0.2) in your code (as shown in the tutorial) keeps you comfortably within limits.
Can I get historical odds too?
Yes — for free. The /historical-odds endpoint returns full price history for any fixture. Check our backtesting tutorial for a complete walkthrough.