Free NFL Odds API Guide: Python Code for Real-Time Betting Data
Learn to fetch real-time NFL lines, spreads, and props with Python. The best free NFL Odds API featuring 300+ bookmakers, native American odds, and historical data.
If you are building a sports betting model, a real-time odds line tracker, an arbitrage bot, or a player props odds screener, you have likely run into a wall. The data is either prohibitively expensive (Sportradar, Genius) or frustratingly limited (APIs that only cover 10 sportsbooks).
You need coverage of the “Big 4” US books (DraftKings, FanDuel, BetMGM, Caesars) plus the offshore sharps (Pinnacle, Circa) to find true market value. And you need it fast.
In this guide, we’ll show you how to use OddsPapi to fetch real-time NFL data using Python.
NFL Odds API Providers Compared (2025)
Before diving into code, here’s how the major NFL odds API providers stack up:
| Provider | Bookmakers | Historical Data | Free Tier |
|---|---|---|---|
| OddsPapi | 300+ (incl. Pinnacle, Circa) | Yes (free) | 200 requests/month |
| The Odds API | ~15 US books | No (paid only) | 500 requests/month |
| SportsDataIO | ~10 books | Limited | Trial only |
| Sportradar | Enterprise-level | Yes | None ($2k+/mo) |
The key difference: OddsPapi includes sharp bookmakers like Pinnacle and Circa that set the true market price. Most “free” APIs only cover retail sportsbooks that copy lines from the sharps with worse juice.
API Terminology: US Sports vs. Global Standards
OddsPapi is a global provider. To use it effectively for US Sports, you need to map a few terms:
| US Concept | API Term | Technical Note |
|---|---|---|
| League | Tournament |
The NFL is a tournament within Sport ID 14 (American Football). |
| Game | Fixture |
You need the fixtureId to pull odds. |
| Moneyline | Market ID 141 |
Markets are returned by ID. 141 is the standard Match Winner. |
| Spread | Asian Handicap Markets |
Multiple IDs based on spread line (see Market IDs section below). |
| Totals | Market ID 148 |
Over/Under on total points scored. |
Step 1: Authentication & Sport IDs
First, get your free API key. Unlike some APIs that use headers, OddsPapi expects the apiKey as a standard query parameter. This makes it easy to test right in your browser.
We also need to target the correct sport. In our system, American Football is Sport ID 14.
import requests
import json
from datetime import datetime, timedelta
API_KEY = "YOUR_API_KEY_HERE"
BASE_URL = "https://api.oddspapi.io/v4"
def get_nfl_tournament_id():
# Pass the API Key as a 'param'.
# Sport ID 14 is American Football.
params = {
"apiKey": API_KEY,
"sportId": 14
}
response = requests.get(f"{BASE_URL}/tournaments", params=params)
if response.status_code == 200:
tournaments = response.json()
# Find the NFL in the list (excluding Simulations)
nfl = next((t for t in tournaments if "NFL" in t.get('tournamentName', '') and "Sim" not in t.get('tournamentName', '')), None)
if nfl:
print(f"Found NFL League ID: {nfl['tournamentId']}")
return nfl['tournamentId']
print("NFL not found.")
return None
nfl_id = get_nfl_tournament_id()
Step 2: Get the Schedule (Fixtures)
The /fixtures endpoint returns a flat list of games. We need to filter this list to find games that actually have odds available (hasOdds: true). This saves you from making useless calls on games that books haven’t posted lines for yet.
def get_nfl_games(tournament_id):
# Get games for the next 7 days
today = datetime.now()
params = {
"apiKey": API_KEY,
"tournamentId": tournament_id,
"from": today.strftime("%Y-%m-%dT00:00:00Z"),
"to": (today + timedelta(days=7)).strftime("%Y-%m-%dT23:59:59Z")
}
response = requests.get(f"{BASE_URL}/fixtures", params=params)
if response.status_code == 200:
fixtures = response.json()
# Filter for games that have odds
games_with_odds = [f for f in fixtures if f.get('hasOdds')]
return games_with_odds
return []
upcoming_games = get_nfl_games(nfl_id)
if upcoming_games:
print(f"Found {len(upcoming_games)} games with odds.")
print(f"Next Game: {upcoming_games[0]['participant1Name']} vs {upcoming_games[0]['participant2Name']}")
Step 3: Parsing the Nested Odds Structure
This is where most developers get stuck. To handle data from 300+ bookmakers without crashing your application, OddsPapi uses a highly optimized, nested JSON structure.
The hierarchy is: Bookmaker -> Markets -> Outcomes -> Players -> Price.
We specifically look for Market ID 141 (Moneyline) to get the “Win” odds for each team.
def get_moneyline_odds(fixture_id):
params = {
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmaker": "draftkings,pinnacle,bet365" # Compare US vs Offshore
}
response = requests.get(f"{BASE_URL}/odds", params=params)
data = response.json()
print(f"\nOdds for Game ID: {fixture_id}")
# Loop through Bookmakers
bookmakers = data.get('bookmakerOdds', {})
for bookie_name, bookie_data in bookmakers.items():
if not bookie_data.get('bookmakerIsActive'):
continue
# Market 141 is the Moneyline
markets = bookie_data.get('markets', {})
if '141' in markets:
outcomes = markets['141'].get('outcomes', {})
# Outcome 141 is Home Team, 142 is Away Team
# We dig deep into the 'players' object to find the 'price'
home_price = outcomes.get('141', {}).get('players', {}).get('0', {}).get('price')
away_price = outcomes.get('142', {}).get('players', {}).get('0', {}).get('price')
if home_price and away_price:
# Convert Decimal to American for display
print(f"{bookie_name}: Home {decimal_to_american(home_price)} | Away {decimal_to_american(away_price)}")
def decimal_to_american(decimal):
if decimal >= 2.0:
return f"+{int((decimal - 1) * 100)}"
else:
return f"{int(-100 / (decimal - 1))}"
# Run it for the first game found
if upcoming_games:
get_moneyline_odds(upcoming_games[0]['fixtureId'])
Output Example
When you run the script above, you get a clean comparison of lines. Notice how easy it is to spot arbitrage or value when you compare DraftKings odds directly against Pinnacle odds:
Odds for Game ID: id1400003160574919
draftkings: Home -135 | Away +115
pinnacle: Home -131 | Away +119
bet365: Home -133 | Away +110
Step 4: Fetching NFL Spreads (Asian Handicap)
NFL spreads are returned as Asian Handicap markets. Unlike moneylines which use a single Market ID, spreads use different Market IDs based on the handicap line.
For a deep dive into Asian Handicap market structure, see our OddsPapi Singbet API and Asian Handicap guide.
Here are the key Market IDs for NFL spreads:
| Market ID | Handicap Line | NFL Example |
|---|---|---|
1072 |
0 (Pick’em) | Even matchup |
1068 |
-0.5 | PK (no push) |
1076 |
-3.5 | Standard NFL spread |
1080 |
-7.5 | One-TD favorite |
def get_spread_odds(fixture_id):
"""Fetch NFL spread odds from multiple bookmakers."""
params = {
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmaker": "pinnacle,draftkings,fanduel"
}
response = requests.get(f"{BASE_URL}/odds", params=params)
data = response.json()
# Common NFL spread market IDs
spread_markets = ['1076', '1080', '1068'] # -3.5, -7.5, -0.5
bookmakers = data.get('bookmakerOdds', {})
for bookie_name, bookie_data in bookmakers.items():
if not bookie_data.get('bookmakerIsActive'):
continue
markets = bookie_data.get('markets', {})
for market_id in spread_markets:
if market_id in markets:
outcomes = markets[market_id].get('outcomes', {})
print(f"{bookie_name} - Market {market_id}:")
for outcome_id, outcome_data in outcomes.items():
price = outcome_data.get('players', {}).get('0', {}).get('price')
handicap = outcome_data.get('players', {}).get('0', {}).get('handicap', 'N/A')
if price:
print(f" Handicap {handicap}: {decimal_to_american(price)}")
Step 5: Fetching Totals (Over/Under)
For totals, use Market ID 148. This gives you the Over/Under lines for total points scored.
def get_totals_odds(fixture_id):
"""Fetch NFL Over/Under odds."""
params = {
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmaker": "pinnacle,draftkings"
}
response = requests.get(f"{BASE_URL}/odds", params=params)
data = response.json()
bookmakers = data.get('bookmakerOdds', {})
for bookie_name, bookie_data in bookmakers.items():
if not bookie_data.get('bookmakerIsActive'):
continue
markets = bookie_data.get('markets', {})
# Market 148 is Totals (Over/Under)
if '148' in markets:
outcomes = markets['148'].get('outcomes', {})
print(f"\n{bookie_name} Totals:")
for outcome_id, outcome_data in outcomes.items():
price = outcome_data.get('players', {}).get('0', {}).get('price')
total_line = outcome_data.get('players', {}).get('0', {}).get('handicap')
outcome_name = "Over" if outcome_id == '391' else "Under"
if price and total_line:
print(f" {outcome_name} {total_line}: {decimal_to_american(price)}")
Why Market IDs Matter
Other APIs use text labels like “h2h” or “spreads”. The problem? If a bookmaker changes “Spread” to “Handicap” in their feed, your bot breaks.
OddsPapi uses stable Integer IDs. Here is your cheat sheet for the NFL:
- 141: Moneyline (Home/Away Winner)
- 148: Totals (Over/Under)
- 1068-1080+: Asian Handicap (Spreads) – varies by line
Historical Odds Data for Backtesting
This is the feature that sets OddsPapi apart from the competition. If you are building a model, you need to know what the odds were in the past. Being able to access historical Pinnacle odds via OddsPapi or Bet365 historical odds via OddsPapi is a must-have when building out your project to compare against softer bookies.
Most “free” APIs lock this behind a paywall. We don’t. You can use the /historical-odds endpoint to fetch closing lines. This is essential for verifying if your strategy would have been profitable last season.
def get_historical_closing_line(fixture_id, bookmaker="pinnacle"):
"""
Fetch the closing line for a completed NFL game.
Essential for backtesting betting models.
"""
params = {
"apiKey": API_KEY,
"fixtureId": fixture_id,
"bookmakers": bookmaker
}
response = requests.get(f"{BASE_URL}/historical-odds", params=params)
if response.status_code == 200:
data = response.json()
# Historical data includes timestamped odds movements
# The last entry is the closing line
return data
return None
# Example: Analyze closing line value
historical = get_historical_closing_line("PAST_GAME_ID")
Beyond the NFL: NCAA College Football
The logic we used above works for any sport. Because we use a dynamic tournamentId system, you can easily switch to College Football.
Simply change your Step 1 search string from "NFL" to "NCAA". You will find the tournament ID for College Football, and the rest of the code (Fixtures, Odds, Parsing) remains exactly the same. This is powerful for developers building “All-in-One” football tools.
For esports betting data, check out our OddsPapi Esports Odds API guide which covers CS2, League of Legends, and Dota 2.
Frequently Asked Questions
What bookmakers does the NFL Odds API include?
OddsPapi covers 300+ bookmakers including all major US sportsbooks (DraftKings, FanDuel, BetMGM, Caesars) plus sharp offshore books like Pinnacle and Circa. This gives you both the retail lines and the “true” market price.
Is the NFL odds data real-time?
Yes. Our REST API provides near real-time odds updates. For high-frequency trading applications, we also offer WebSocket connections that push line movements as they happen.
Can I access NFL player props?
Yes. Player props are available through additional Market IDs. Contact our team for documentation on prop market structures, or explore the API response to discover available markets for each fixture.
How do I get historical NFL odds for backtesting?
Use the /historical-odds endpoint with any past fixture ID. This returns timestamped odds movements so you can see opening lines, closing lines, and everything in between. Historical data is included in the free tier.
Ready to Build Your NFL Betting Tool?
You now have Python scripts that authenticate correctly, find the NFL dynamically, and parse the deep nested JSON to extract moneylines, spreads, and totals. Stop scraping websites and start building with a professional API.