If you want to build a sports betting app, a price comparison tool, or a data-driven model, you need reliable odds data. An odds API gives you structured, real-time betting lines from dozens of bookmakers in a single JSON response. Python makes it easy to fetch, parse, and store that data.
This tutorial walks through the entire process using The Odds API, one of the most popular sports betting APIs available. By the end, you will have working Python code that fetches live odds, compares bookmaker prices, and saves everything to a local database. If you are new to pulling sports data with code, start with our beginner Python tutorial first.
Prerequisites
Before writing any code, make sure you have the following three things ready.
Python 3.8+
Any recent version of Python will work. Open a terminal and verify your installation:
python --version
If you see Python 3.8 or higher, you are good to go. If not, download the latest version from python.org.
API Key (Free Tier)
The Odds API offers a free tier with 500 requests per month. That is enough to follow this tutorial and build a working prototype. You will grab your key in Step 1 below.
Required Libraries
You need two Python libraries: requests for making HTTP calls and tabulate for formatting output as tables. Install both with pip:
pip install requests tabulate
That is it for setup. Three dependencies, zero configuration files.
Step 1: Get Your API Key
Head to the-odds-api.com and sign up for a free account. The process takes about 30 seconds:
- Enter your email address on the homepage
- Check your inbox for a confirmation email
- Your API key will be included in that email
Copy the key and store it somewhere safe. You will pass it as a query parameter on every API call. Never hardcode it directly in files you share publicly. A simple approach is to use an environment variable:
import os
API_KEY = os.environ.get("ODDS_API_KEY", "your_api_key_here")
Set the variable in your terminal before running scripts:
export ODDS_API_KEY="abc123your_key_here"
The free plan gives you 500 requests per month. Each request to the /odds endpoint costs 1 credit per region per market. A single call fetching h2h odds for the us region costs exactly 1 credit. You can track your remaining balance through response headers, which we will cover in Step 2.
Step 2: Make Your First API Call
The Odds API v4 base URL is https://api.the-odds-api.com/v4. Every endpoint requires your api_key as a query parameter. Let us start with two fundamental calls.
List Available Sports
The /sports endpoint returns every sport currently in season. This call does not cost any credits, so you can use it freely:
import requests
import os
API_KEY = os.environ.get("ODDS_API_KEY", "your_api_key_here")
BASE_URL = "https://api.the-odds-api.com/v4"
response = requests.get(f"{BASE_URL}/sports", params={
"api_key": API_KEY
})
if response.status_code == 200:
sports = response.json()
for sport in sports:
print(f"{sport['key']:40s} {sport['title']}")
else:
print(f"Error: {response.status_code} - {response.text}")
This prints a list like:
americanfootball_nfl NFL
basketball_nba NBA
baseball_mlb MLB
soccer_epl EPL
icehockey_nhl NHL
The key field is what you pass to other endpoints. The value upcoming is a special key that returns the next 8 events across all sports, which is useful for testing.
Fetch Odds for a Sport
Now let us pull live odds. This is the core endpoint and it does consume credits:
SPORT = "basketball_nba"
REGIONS = "us"
MARKETS = "h2h"
response = requests.get(f"{BASE_URL}/sports/{SPORT}/odds", params={
"api_key": API_KEY,
"regions": REGIONS,
"markets": MARKETS,
"oddsFormat": "american",
"dateFormat": "iso",
})
if response.status_code == 200:
events = response.json()
print(f"Found {len(events)} upcoming events")
# Check your remaining quota
remaining = response.headers.get("x-requests-remaining")
used = response.headers.get("x-requests-used")
print(f"Quota: {used} used, {remaining} remaining")
else:
print(f"Error: {response.status_code} - {response.text}")
Key parameters to know:
regions: Controls which bookmakers appear. Options includeus,us2,uk,au, andeu. You can combine them with commas likeus,ukbut each region costs 1 additional credit.markets: The type of bet.h2his moneyline,spreadsis point spread, andtotalsis over/under. Multiple markets can be comma-separated.oddsFormat: Eitherdecimal(1.91) oramerican(-110). American is standard in the US market.
The response headers x-requests-remaining and x-requests-used let you monitor your quota in real time. Always check these to avoid hitting your limit unexpectedly.
Step 3: Parse and Display the Data
The raw JSON response contains nested bookmaker and market data. Let us extract and format it.
Extract Bookmaker Odds
Each event in the response contains an array of bookmakers, and each bookmaker contains an array of markets with outcomes. Here is how to drill into that structure:
for event in events:
home = event["home_team"]
away = event["away_team"]
start = event["commence_time"]
print(f"\n{away} @ {home}")
print(f"Start: {start}")
for bookmaker in event["bookmakers"]:
bk_name = bookmaker["title"]
for market in bookmaker["markets"]:
if market["key"] == "h2h":
outcomes = market["outcomes"]
odds_str = " | ".join(
f"{o['name']}: {o['price']}" for o in outcomes
)
print(f" {bk_name:20s} {odds_str}")
This produces output like:
Lakers @ Celtics
Start: 2026-02-13T00:00:00Z
FanDuel Celtics: -180 | Lakers: +155
DraftKings Celtics: -175 | Lakers: +150
BetMGM Celtics: -170 | Lakers: +145
Compare Across Bookmakers
Finding the best price across bookmakers is one of the main reasons developers use a betting API. Here is a function that extracts the best odds for each outcome:
def find_best_odds(event):
best = {}
for bookmaker in event["bookmakers"]:
for market in bookmaker["markets"]:
if market["key"] == "h2h":
for outcome in market["outcomes"]:
name = outcome["name"]
price = outcome["price"]
if name not in best or price > best[name]["price"]:
best[name] = {
"price": price,
"bookmaker": bookmaker["title"]
}
return best
for event in events:
print(f"\n{event['away_team']} @ {event['home_team']}")
best = find_best_odds(event)
for team, info in best.items():
print(f" Best for {team}: {info['price']} ({info['bookmaker']})")
With American odds, a higher number is better for the bettor on both sides. For positive odds (+150 vs +145), +150 pays more. For negative odds (-170 vs -180), -170 costs less for the same payout.
Format as a Table
The tabulate library makes console output much easier to read:
from tabulate import tabulate
rows = []
for event in events:
best = find_best_odds(event)
for team, info in best.items():
rows.append([
f"{event['away_team']} @ {event['home_team']}",
team,
info["price"],
info["bookmaker"]
])
print(tabulate(rows, headers=["Game", "Team", "Best Odds", "Bookmaker"]))
Output:
Game Team Best Odds Bookmaker
------------------------ -------- ---------- ----------
Lakers @ Celtics Celtics -170 BetMGM
Lakers @ Celtics Lakers +155 FanDuel
Warriors @ Bucks Bucks -145 DraftKings
Warriors @ Bucks Warriors +130 FanDuel
Step 4: Store Data for Analysis
Fetching odds once is useful. Storing them over time is where things get interesting. Historical price movements reveal line shopping opportunities, closing line value, and market inefficiencies.
Save to CSV/JSON
For quick storage, CSV or JSON work fine. Here is a CSV approach that appends new data on each run:
import csv
from datetime import datetime
def save_odds_csv(events, filename="odds_history.csv"):
file_exists = os.path.exists(filename)
with open(filename, "a", newline="") as f:
writer = csv.writer(f)
if not file_exists:
writer.writerow([
"timestamp", "sport", "game", "bookmaker",
"market", "team", "price"
])
timestamp = datetime.now().isoformat()
for event in events:
game = f"{event['away_team']} @ {event['home_team']}"
for bookmaker in event["bookmakers"]:
for market in bookmaker["markets"]:
for outcome in market["outcomes"]:
writer.writerow([
timestamp,
event["sport_key"],
game,
bookmaker["title"],
market["key"],
outcome["name"],
outcome["price"]
])
print(f"Saved {len(events)} events to {filename}")
save_odds_csv(events)
Each run appends rows, building a time series of odds snapshots. You can also save as JSONL (one JSON object per line) for easier programmatic access:
import json
def save_odds_jsonl(events, filename="odds_history.jsonl"):
timestamp = datetime.now().isoformat()
with open(filename, "a") as f:
for event in events:
for bookmaker in event["bookmakers"]:
for market in bookmaker["markets"]:
for outcome in market["outcomes"]:
record = {
"timestamp": timestamp,
"sport": event["sport_key"],
"home": event["home_team"],
"away": event["away_team"],
"bookmaker": bookmaker["title"],
"market": market["key"],
"team": outcome["name"],
"price": outcome["price"]
}
f.write(json.dumps(record) + "\n")
Build a Simple Database
For anything beyond a quick prototype, SQLite gives you structured queries without any external server:
import sqlite3
def init_db(db_path="odds.db"):
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS odds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
sport TEXT,
home_team TEXT,
away_team TEXT,
commence_time TEXT,
bookmaker TEXT,
market TEXT,
outcome_name TEXT,
price REAL
)
""")
conn.commit()
return conn
def store_odds(conn, events):
timestamp = datetime.now().isoformat()
rows = []
for event in events:
for bookmaker in event["bookmakers"]:
for market in bookmaker["markets"]:
for outcome in market["outcomes"]:
rows.append((
timestamp,
event["sport_key"],
event["home_team"],
event["away_team"],
event["commence_time"],
bookmaker["title"],
market["key"],
outcome["name"],
outcome["price"]
))
conn.executemany("""
INSERT INTO odds (
timestamp, sport, home_team, away_team,
commence_time, bookmaker, market, outcome_name, price
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", rows)
conn.commit()
print(f"Stored {len(rows)} odds records")
conn = init_db()
store_odds(conn, events)
Now you can run SQL queries against your stored data:
# Find the best historical odds for a team
cursor = conn.execute("""
SELECT bookmaker, price, timestamp
FROM odds
WHERE outcome_name = 'Celtics' AND market = 'h2h'
ORDER BY price DESC
LIMIT 5
""")
for row in cursor.fetchall():
print(f" {row[0]:20s} {row[1]:>6} at {row[2]}")
Next Steps
You now have a working pipeline that fetches odds, parses them, and stores the results. Here are three ways to take it further.
Add More Sports
Replace the SPORT variable with any key from the /sports endpoint. The Odds API covers NFL, NBA, MLB, NHL, soccer leagues, tennis, MMA, and many more. You can also use upcoming to pull the next 8 events across all sports at once:
SPORT = "upcoming"
Request multiple markets in a single call to get moneyline, spreads, and totals together:
MARKETS = "h2h,spreads,totals"
Each additional market costs 1 credit per region, so a call with 3 markets and 1 region costs 3 credits. For a deeper look at what is available across different providers, see our comparison of the best sports betting APIs.
Build a Dashboard
Once you have data flowing into SQLite, you can build a simple web dashboard with Flask or Streamlit to visualize odds movements in real time. A minimal Streamlit app takes about 20 lines of code:
import streamlit as st
import sqlite3
import pandas as pd
conn = sqlite3.connect("odds.db")
df = pd.read_sql("SELECT * FROM odds ORDER BY timestamp DESC LIMIT 500", conn)
st.title("Live Odds Dashboard")
sport_filter = st.selectbox("Sport", df["sport"].unique())
filtered = df[df["sport"] == sport_filter]
st.dataframe(filtered)
That gives you a filterable table updated every time you refresh the page. For a production dashboard, you would add auto-refresh, charts, and alerts when lines move beyond a threshold.
Automate with Cron
To build a meaningful dataset, schedule your script to run at regular intervals. A cron job that pulls NBA odds every 15 minutes looks like this:
*/15 * * * * cd /path/to/project && python fetch_odds.py >> logs/cron.log 2>&1
At 4 calls per hour, that is 96 calls per day, which fits comfortably within the free tier for a single sport and region. If you need higher frequency or more coverage, paid plans start at around $25 per month with 10,000+ credits. Our complete guide to sports betting APIs covers pricing and rate limits across all major providers.
FAQ
Is the odds API free?
Yes. The Odds API offers a free tier with 500 requests per month. Each request to the odds endpoint costs 1 credit per region per market. That means a single call for NBA moneyline odds from US bookmakers costs 1 credit. The free tier is enough to build and test a working application. Paid plans range from around $25 to $249 per month and offer between 10,000 and 250,000+ monthly credits.
How many requests can I make?
On the free plan, 500 per month. The /sports and /events endpoints do not count against your quota, so you can call those as often as you need. The /odds endpoint is where credits get consumed. A request for 2 markets across 2 regions costs 4 credits (2 x 2). You can monitor your balance in real time using the x-requests-remaining and x-requests-used response headers that come back with every API call.
What Python libraries do I need?
Just requests for making HTTP calls to the API. That is the only hard requirement. This tutorial also uses tabulate for formatted console output, sqlite3 (included in Python’s standard library) for local storage, and csv/json (also standard library) for file-based storage. No heavy frameworks or complex dependencies are needed.