Skip to main content
How to Use a Sports Betting API with Python: A Complete Guide
Back to Blog

How to Use a Sports Betting API with Python: A Complete Guide

James Whitfield

James Whitfield

•9 min read

Everything you need to start pulling live sports betting odds with Python, from your first API call to a working value bet finder.

Why Python Is the Best Language for a Sports Betting API

If you're looking for a sports betting API you can use with Python, you've picked the right combination. Python has become the default language for anyone working with data, and sports betting is no exception. The requests library makes HTTP calls trivially easy, pandas is perfect for organising odds into tables, and the language's clean syntax means you can go from zero to a working odds pipeline in an afternoon. Whether you're building a value betting model, an arbitrage scanner, or just a personal odds tracker, Python removes most of the friction between you and the data.

In this guide we'll walk through the complete workflow for pulling sports betting odds using Odds-API.io, from fetching available leagues all the way through to a simple value bet detector. Every snippet is standalone and ready to copy and run.

Prerequisites

  • Python 3.8+ installed
  • An Odds-API key (grab a free one at odds-api.io, takes 30 seconds)

Install the requests library if you don't have it:

pip install requests

That's it. No complex setup.

How a Sports Betting Odds API Works

Odds-API is a straightforward REST API. Every request passes your API key as a query parameter and returns JSON. The workflow follows three steps: fetch leagues to get the available competition slugs, use a slug to fetch upcoming fixtures and their event IDs, then use an event ID to fetch the full odds for that match. Think of it as a chain: leagues, events, odds.

If you need real-time streaming data rather than request-based polling, Odds-API also offers a WebSocket feed.

Script 1: Fetch Available Leagues

Before you can pull odds, you need to know which leagues are available and what their slugs are. The /leagues endpoint returns the full list along with a live event count, so you can filter out competitions with nothing currently scheduled.

import requests
import json

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.odds-api.io/v3"

def get_leagues(sport, active_only=True):
    url = f"{BASE_URL}/leagues"
    params = {
        "apiKey": API_KEY,
        "sport": sport,
        "all": "true"
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    leagues = response.json()

    if active_only:
        leagues = [l for l in leagues if l["eventsCount"] > 0]

    return leagues

leagues = get_leagues("football")
print(json.dumps(leagues, indent=2))

Response:

[
  {
    "name": "England - FA Cup",
    "slug": "england-fa-cup",
    "eventsCount": 8
  },
  {
    "name": "England - League One",
    "slug": "england-league-one",
    "eventsCount": 74
  },
  {
    "name": "England - Premier League",
    "slug": "england-premier-league",
    "eventsCount": 39
  }
  // ...
]

The active_only=True flag is worth keeping in place. It filters out leagues with no upcoming fixtures so you're not passing dead slugs into subsequent calls.

Script 2: Fetch Upcoming Fixtures

Once you have a league slug, pass it to the /events endpoint to get the list of upcoming matches. Each event comes back with a unique ID that you'll use to pull odds in the next step.

import requests
import json

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.odds-api.io/v3"

def get_events(sport, league_slug, bookmaker="Bet365"):
    url = f"{BASE_URL}/events"
    params = {
        "apiKey": API_KEY,
        "sport": sport,
        "league": league_slug,
        "status": "pending",
        "bookmaker": bookmaker
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()

events = get_events("football", "england-premier-league")
print(json.dumps(events, indent=2))

Response:

{
  "id": 66905890,
  "home": "Everton FC",
  "away": "Manchester United",
  "homeId": 48,
  "awayId": 35,
  "date": "2026-02-23T20:00:00Z",
  "sport": {
    "name": "Football",
    "slug": "football"
  },
  "league": {
    "name": "England - Premier League",
    "slug": "england-premier-league"
  },
  "status": "pending",
  "scores": {
    "home": 0,
    "away": 0
  }
}

Script 3: Fetch Odds for a Match

Take any event ID from the previous step and pass it to the /odds endpoint. A single call returns every market that bookmaker offers for that fixture.

import requests
import json

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.odds-api.io/v3"

def get_odds(event_id, bookmaker="Bet365"):
    url = f"{BASE_URL}/odds"
    params = {
        "apiKey": API_KEY,
        "eventId": event_id,
        "bookmakers": bookmaker
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()

odds_data = get_odds(61301077)
print(json.dumps(odds_data, indent=2))

Response (truncated):

{
  "id": 61301077,
  "home": "Wolverhampton Wanderers",
  "away": "Aston Villa",
  "date": "2026-02-27T20:00:00Z",
  "status": "pending",
  "bookmakers": {
    "Bet365": [
      {
        "name": "ML",
        "updatedAt": "2026-02-23T11:46:23.164Z",
        "odds": [
          {
            "home": "4.100",
            "draw": "3.500",
            "away": "1.850"
          }
        ]
      },
      {
        "name": "Totals",
        "updatedAt": "2026-02-23T11:46:23.164Z",
        "odds": [
          {
            "hdp": 2.5,
            "over": "1.825",
            "under": "2.025"
          }
        ]
      }
      // ...
    ]
  }
}

The full response for a single match contains a large number of markets. For Wolverhampton vs Aston Villa, Bet365 returned the following:

ML, Draw No Bet, Double Chance, Spread, Totals, Goals Over/Under, Both Teams To Score, Spread HT, Totals HT, Team Total Home, Team Total Away, European Handicap, Anytime Goalscorer, Both Teams To Score 2H, Both Teams To Score HT, Number of Goals In Match, Specials, Multi Scorers, Half Time Result, First 10 Minutes, Team Goalscorer, 1st Half Handicap, Alternative Goal Line, Exact Total Goals, Goal Method, Alternative Total Goals, Player To Score or Assist, Player Shots, Other Player Props, Alternative Asian Handicap.

You also get a Bet365 (no latency) field alongside the standard Bet365 data. This isn't a separate bookmaker, it's an additional feed within the Bet365 response that provides real-time, zero-latency odds for the ML, Spread and Totals markets across football, basketball and handball. If you're building anything time-sensitive like a live betting tool or an odds movement tracker, this is the field you want to read from.

Script 4: Pull Multiple Markets at Once

Because the full odds response contains every available market in a single call, there's no need to make separate requests per market type. Here's how to cleanly extract several at once:

import requests
import json

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.odds-api.io/v3"

MARKETS_TO_SHOW = ["ML", "Totals", "Both Teams To Score", "Draw No Bet"]

def get_odds(event_id, bookmaker="Bet365"):
    url = f"{BASE_URL}/odds"
    params = {
        "apiKey": API_KEY,
        "eventId": event_id,
        "bookmakers": bookmaker
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()

def get_key_markets(event_id, bookmaker="Bet365"):
    odds_data = get_odds(event_id, bookmaker)
    markets = odds_data["bookmakers"].get(bookmaker, [])

    filtered_markets = {}
    for market in markets:
        if market["name"] in MARKETS_TO_SHOW:
            filtered_markets[market["name"]] = market["odds"]

    result = {
        "id": odds_data["id"],
        "home": odds_data["home"],
        "away": odds_data["away"],
        "date": odds_data["date"],
        "status": odds_data["status"],
        "sport": odds_data["sport"],
        "league": odds_data["league"],
        "urls": odds_data.get("urls", {}),
        "markets": filtered_markets
    }

    return result

output = get_key_markets(61301077)
print(json.dumps(output, indent=2))

Response:

{
  "id": 61301077,
  "home": "Wolverhampton Wanderers",
  "away": "Aston Villa",
  "date": "2026-02-27T20:00:00Z",
  "status": "pending",
  "sport": {
    "name": "Football",
    "slug": "football"
  },
  "league": {
    "name": "England - Premier League",
    "slug": "england-premier-league"
  },
  "urls": {
    "Bet365": "https://www.bet365.com/#/AC/B1/C1/D8/E189124719/F3/I8/"
  },
  "markets": {
    "ML": [
      {
        "home": "3.900",
        "draw": "3.700",
        "away": "1.900"
      }
    ],
    "Draw No Bet": [
      {
        "home": "2.750",
        "away": "1.400"
      }
    ],
    "Totals": [
      {
        "hdp": 2.5,
        "over": "1.825",
        "under": "2.025"
      }
    ],
    "Both Teams To Score": [
      {
        "yes": "1.700",
        "no": "2.050"
      }
    ]
  }
}

Script 5: Simple Value Bet Finder

This is where the data becomes genuinely useful. Rather than relying on your own probability estimates, a smarter approach is to use the odds from a sharp bookmaker as your reference price. Sharp books like SingBet set highly efficient lines with minimal margin, which means their implied probabilities are a close approximation of the true probability of each outcome. If a soft bookmaker like Bet365 is offering a significantly higher price on the same outcome, that's a potential edge.

However, even sharp bookmakers build a margin into their odds, typically 2-4% across a three-way market. If you compare Bet365 directly against raw SingBet odds without accounting for this, you'll be using slightly inflated implied probabilities as your baseline, which makes value harder to find and easier to miss. The correct approach is to remove SingBet's margin first to get the "fair" odds or the true market price with no house edge and then compare Bet365 against those.

Removing the margin is straightforward. You sum the implied probabilities across all outcomes, then divide each one by that total to normalise them to 100%. The fair odds for each outcome is simply 1 / normalised_probability. This is known as the proportional devig method and is the simplest starting point.

It's worth knowing, however, that sharp books don't always distribute their margin equally across all outcomes. There is a well-documented favourite/longshot bias in betting markets, where bookmakers tend to apply more margin to longer-odds outcomes than to short-priced favourites. This means the proportional method can slightly overestimate the true probability of outsiders and underestimate it for favourites. More sophisticated devigging methods like Logarithmic, Power, or Shin account for this by distributing the margin unevenly in a way that better reflects how markets are actually priced. For a basic ML scanner the proportional method is a reasonable starting point, but if you're building a serious value betting model it's worth researching these approaches further as the differences become more meaningful the further out you go in the odds range.

import requests
import json

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.odds-api.io/v3"

MIN_EDGE = 0.03

def get_odds(event_id, bookmakers):
    response = requests.get(f"{BASE_URL}/odds", params={
        "apiKey": API_KEY,
        "eventId": event_id,
        "bookmakers": ",".join(bookmakers)
    })
    response.raise_for_status()
    return response.json()

def get_ml(odds_data, bookmaker):
    for market in odds_data["bookmakers"].get(bookmaker, []):
        if market["name"] == "ML":
            return market["odds"][0]
    return None

def remove_margin(ml):
    outcomes = ["home", "draw", "away"]
    implied_probs = [1 / float(ml[o]) for o in outcomes]
    total = sum(implied_probs)
    fair_probs = [p / total for p in implied_probs]
    fair_odds = [1 / p for p in fair_probs]
    return dict(zip(outcomes, fair_odds))

def find_value_bets(event_id, sharp="SingBet", soft="Bet365"):
    odds_data = get_odds(event_id, [sharp, soft])

    # Build a filtered response showing only ML for each bookmaker
    ml_response = {
        "id": odds_data["id"],
        "home": odds_data["home"],
        "away": odds_data["away"],
        "date": odds_data["date"],
        "bookmakers": {}
    }
    for bookie in [sharp, soft]:
        for market in odds_data["bookmakers"].get(bookie, []):
            if market["name"] == "ML":
                ml_response["bookmakers"][bookie] = market
                break

    print("Raw API response (ML only):")
    print(json.dumps(ml_response, indent=2))
    print()

    sharp_ml = get_ml(odds_data, sharp)
    soft_ml = get_ml(odds_data, soft)

    if not sharp_ml or not soft_ml:
        print("ML odds not available for one or both bookmakers.")
        return

    fair_odds = remove_margin(sharp_ml)

    implied_probs = [1 / float(sharp_ml[o]) for o in ["home", "draw", "away"]]
    margin = (sum(implied_probs) - 1) * 100

    print(f"SingBet margin: {margin:.1f}%")
    print()
    print(f"{'Outcome':<10} {'Fair Odds':>10} {soft:>10} {'Edge':>8}")
    print("-" * 45)

    for outcome in ["home", "draw", "away"]:
        soft_odds = float(soft_ml[outcome])
        fair = fair_odds[outcome]
        edge = (soft_odds / fair - 1)

        flag = " *** VALUE" if edge >= MIN_EDGE else ""
        print(f"{outcome:<10} {fair:>10.3f} {soft_odds:>10.3f} {edge:>+8.1%}{flag}")

find_value_bets(61301077)

API response:

{
  "id": 61301077,
  "home": "Wolverhampton Wanderers",
  "away": "Aston Villa",
  "date": "2026-02-27T20:00:00Z",
  "bookmakers": {
    "SingBet": {
      "name": "ML",
      "updatedAt": "2026-02-23T10:21:31.367Z",
      "odds": [
        {
          "home": "3.700",
          "draw": "3.950",
          "away": "1.930"
        }
      ]
    },
    "Bet365": {
      "name": "ML",
      "updatedAt": "2026-02-23T13:58:32.194Z",
      "odds": [
        {
          "home": "3.900",
          "draw": "3.700",
          "away": "1.900"
        }
      ]
    }
  }
}
```

Output:
```
SingBet margin: 4.2%

Outcome      Fair Odds     Bet365     Edge
---------------------------------------------
home            3.854      3.900   +1.2%
draw            4.114      3.700   -10.1%
away            2.010      1.900   -5.5%

Once you strip SingBet's margin out, the picture changes. The home outcome still shows Bet365 above the fair price, but only by 1.5% which is not enough to clear the 3% threshold. Without removing the margin, that same outcome appeared to show a 5.4% edge. That's the difference between acting on noise and acting on a real signal.

Full Python Script: Sports Betting API Pipeline

Here's everything combined into one complete script. It fetches leagues, loops through fixtures, pulls odds from both SingBet and Bet365, removes the sharp book margin to calculate fair odds, and flags any genuine value bets. Set your target leagues and bookmakers at the top and run it.

import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.odds-api.io/v3"

TARGET_LEAGUES = ["england-premier-league", "england-championship"]
SHARP = "SingBet"
SOFT = "Bet365"
MIN_EDGE = 0.03


def get_leagues(sport, active_only=True):
    response = requests.get(f"{BASE_URL}/leagues", params={
        "apiKey": API_KEY,
        "sport": sport,
        "all": "true"
    })
    response.raise_for_status()
    leagues = response.json()
    if active_only:
        leagues = [l for l in leagues if l["eventsCount"] > 0]
    return leagues


def get_events(sport, league_slug, bookmaker=SOFT):
    response = requests.get(f"{BASE_URL}/events", params={
        "apiKey": API_KEY,
        "sport": sport,
        "league": league_slug,
        "status": "pending",
        "bookmaker": bookmaker
    })
    response.raise_for_status()
    return response.json()


def get_odds(event_id, bookmakers):
    response = requests.get(f"{BASE_URL}/odds", params={
        "apiKey": API_KEY,
        "eventId": event_id,
        "bookmakers": ",".join(bookmakers)
    })
    response.raise_for_status()
    return response.json()


def get_ml(odds_data, bookmaker):
    for market in odds_data["bookmakers"].get(bookmaker, []):
        if market["name"] == "ML":
            return market["odds"][0]
    return None


def remove_margin(ml):
    """Convert raw odds to fair odds by removing the bookmaker margin."""
    outcomes = ["home", "draw", "away"]
    implied_probs = [1 / float(ml[o]) for o in outcomes]
    total = sum(implied_probs)
    fair_probs = [p / total for p in implied_probs]
    fair_odds = [1 / p for p in fair_probs]
    return dict(zip(outcomes, fair_odds))


def find_value_bets(sharp_ml, soft_ml):
    fair_odds = remove_margin(sharp_ml)
    found = False

    for outcome in ["home", "draw", "away"]:
        soft_odds = float(soft_ml[outcome])
        fair = fair_odds[outcome]
        edge = (soft_odds / fair - 1)

        if edge >= MIN_EDGE:
            print(f"  āœ… VALUE — {outcome.upper()} @ {soft_odds} "
                  f"({SOFT} vs fair odds edge: +{edge:.1%})")
            found = True

    if not found:
        print(f"  šŸ”Ž No value found.")


# --- Run the pipeline ---

for league_slug in TARGET_LEAGUES:
    print(f"\nšŸŒ {league_slug.upper()}\n")
    events = get_events("football", league_slug)

    for event in events[:3]:
        print(f"šŸŸ  {event['home']} vs {event['away']} ({event['date'][:10]})")

        odds_data = get_odds(event["id"], [SHARP, SOFT])
        sharp_ml = get_ml(odds_data, SHARP)
        soft_ml = get_ml(odds_data, SOFT)

        if sharp_ml and soft_ml:
            implied_probs = [1 / float(sharp_ml[o]) for o in ["home", "draw", "away"]]
            margin = (sum(implied_probs) - 1) * 100
            print(f"  šŸ“™ {SOFT}  — Home: {soft_ml['home']} | "
                  f"Draw: {soft_ml['draw']} | Away: {soft_ml['away']}")
            print(f"  šŸ“— {SHARP} — Home: {sharp_ml['home']} | "
                  f"Draw: {sharp_ml['draw']} | Away: {sharp_ml['away']}")
            print(f"  šŸ“Š {SHARP} Margin: {margin:.1f}%")
            find_value_bets(sharp_ml, soft_ml)
        else:
            print("  ML odds not available for one or both bookmakers.")

        print()

Response:

šŸŒ ENGLAND-PREMIER-LEAGUE

šŸŸ  Everton FC vs Manchester United (2026-02-23)
  šŸ“™ Bet365  — Home: 3.750 | Draw: 3.800 | Away: 1.900
  šŸ“— SingBet — Home: 3.950 | Draw: 3.800 | Away: 1.910
  šŸ“Š SingBet Margin: 4.0%
  šŸ”Ž No value found.

šŸŸ  Wolverhampton Wanderers vs Aston Villa (2026-02-27)
  šŸ“™ Bet365  — Home: 3.900 | Draw: 3.700 | Away: 1.900
  šŸ“— SingBet — Home: 3.700 | Draw: 3.950 | Away: 1.930
  šŸ“Š SingBet Margin: 4.2%
  šŸ”Ž No value found.

šŸŸ  AFC Bournemouth vs Sunderland AFC (2026-02-28)
  šŸ“™ Bet365  — Home: 1.800 | Draw: 3.600 | Away: 4.333
  šŸ“— SingBet — Home: 1.820 | Draw: 4.000 | Away: 4.150
  šŸ“Š SingBet Margin: 4.0%
  šŸ”Ž No value found.

šŸŒ ENGLAND-CHAMPIONSHIP

šŸŸ  Blackburn Rovers vs Bristol City (2026-02-24)
  šŸ“™ Bet365  — Home: 2.250 | Draw: 3.250 | Away: 3.200
  šŸ“— SingBet — Home: 2.280 | Draw: 3.150 | Away: 3.100
  šŸ“Š SingBet Margin: 7.9%
  šŸ”Ž No value found.

šŸŸ  Wrexham AFC vs Portsmouth FC (2026-02-24)
  šŸ“™ Bet365  — Home: 2.000 | Draw: 3.300 | Away: 3.700
  šŸ“— SingBet — Home: 2.070 | Draw: 3.250 | Away: 3.450
  šŸ“Š SingBet Margin: 8.1%
  šŸ”Ž No value found.

šŸŸ  West Bromwich Albion vs Charlton Athletic (2026-02-24)
  šŸ“™ Bet365  — Home: 2.050 | Draw: 3.400 | Away: 3.600
  šŸ“— SingBet — Home: 2.070 | Draw: 3.250 | Away: 3.450
  šŸ“Š SingBet Margin: 8.1%
  šŸ”Ž No value found.

What to Build Next

The scripts above are a solid foundation for anything odds-related in Python. From here you could store snapshots in a database to track line movement over time, extend the value bet finder to scan every league and every market automatically, or set up a Telegram alert that pings you when an edge appears. If you want to improve the accuracy of your fair odds further, it's worth exploring more advanced devigging methods. There's a growing body of literature on Logarithmic, Power, and Shin methods that is worth digging into.

Another natural next step is building an arbitrage scanner. Where the value bet finder looks for soft books mispricing a single outcome relative to a sharp, an arbitrage scanner looks for situations where the combined odds across multiple bookmakers guarantee a profit regardless of the result.

Odds-API covers hundreds of leagues across football, basketball, American football, tennis and more, with data from 250+ bookmakers. Full documentation is at docs.odds-api.io.

Get your free API key at odds-api.io and start pulling sports betting odds with Python in under five minutes.