How to Build a Betting App with Python (Beginner Guide)

Build a Betting App - OddsPapi API Blog
How To Guides May 13, 2026

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:

  1. Lists available sports from the API
  2. Fetches upcoming fixtures (matches) for a sport you pick
  3. Pulls live odds from three bookmakers — Pinnacle, Bet365, and DraftKings
  4. Displays a side-by-side comparison table
  5. 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 requests library: 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 calls
  • slug — 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 from and to parameters use ISO 8601 format. The maximum range is 10 days.
  • hasOdds: true means 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 (like id1000001761301147) that you’ll use to fetch odds.
  • Teams are called participant1Name (home) and participant2Name (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.