MMA Odds API: Live UFC Moneylines from 46 Bookmakers (Free Tier)

MMA Odds API - OddsPapi API Blog
How To Guides May 8, 2026

Looking for a UFC odds API? There isn’t one. The UFC doesn’t expose betting data publicly, and most third-party odds APIs top out at 20–40 soft bookmakers with zero sharp coverage.

OddsPapi covers MMA with 46 bookmakers — including Pinnacle, Polymarket, crypto books like Stake and BC.Game, and US-regulated books like BetRivers and BetMGM. Free tier. No scraping. One API call.

This guide walks you through pulling live UFC moneylines with Python, comparing prices across all 46 books, and spotting value against the Pinnacle sharp line.

Why Most Odds APIs Fall Short on MMA

MMA sits in an awkward spot. It’s too niche for the big enterprise feeds to prioritize, but too popular for developers to ignore. The result:

  • Scraping — fragile, rate-limited, legally grey. One DOM change breaks your pipeline.
  • Generic APIs — The Odds API covers ~40 bookmakers total, mostly US softs. No Pinnacle, no crypto, no Asian books.
  • Enterprise feeds — Sportradar/Betgenius have MMA, but you’re looking at five-figure annual contracts.

OddsPapi sits in the middle: 350+ bookmakers across 69 sports, including 46 that actively price UFC fights. Free tier with historical data included.

Scraping Generic API OddsPapi
MMA bookmakers 1 at a time 10–15 46
Sharps (Pinnacle) No Sometimes Yes
Crypto books Maybe No Yes (Stake, BC.Game, Rollbit)
Prediction markets No No Yes (Polymarket)
Free tier N/A Limited Yes + free historicals
Real-time Polling Polling WebSockets available

Step 1: Get Your API Key

Sign up at oddspapi.io and grab your API key. The free tier is enough for everything in this tutorial.

Authentication is a query parameter — no headers, no OAuth:


import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

# Test your key
r = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
print(r.status_code)  # 200

Step 2: Discover UFC Fixtures

MMA is sportId=20 in the OddsPapi API. The /fixtures endpoint returns upcoming bouts with participant names, card info, and whether odds are available.


import requests
from datetime import datetime, timedelta

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

# Get MMA fixtures for the next 10 days
today = datetime.utcnow().strftime("%Y-%m-%d")
end = (datetime.utcnow() + timedelta(days=10)).strftime("%Y-%m-%d")

r = requests.get(f"{BASE_URL}/fixtures", params={
    "apiKey": API_KEY,
    "sportId": 20,
    "from": today,
    "to": end
})

fixtures = r.json()

for f in fixtures:
    if f.get("hasOdds"):
        print(f"{f['participant1Name']} vs {f['participant2Name']}")
        print(f"  Card: {f['tournamentName']}")
        print(f"  Starts: {f['startTime'][:16]}")
        print(f"  Fixture ID: {f['fixtureId']}")
        print()

Key terminology: OddsPapi uses “fixture” (not “game” or “fight”), “participant” (not “fighter” or “team”), and “tournament” (not “event” or “card”). The categoryName field tells you the promotion — “UFC”, “Bellator”, “PFL”, etc.

Only fixtures with hasOdds: true will return pricing data. The rest are scheduled but not yet being priced by bookmakers.

Step 3: Pull Moneylines for a Fight

The /odds endpoint returns live odds from every bookmaker pricing a fixture. For MMA, the moneyline (winner) market uses market ID 201.


import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

fixture_id = "YOUR_FIXTURE_ID"  # From Step 2

r = requests.get(f"{BASE_URL}/odds", params={
    "apiKey": API_KEY,
    "fixtureId": fixture_id
})

data = r.json()
fighter1 = data["participant1Name"]
fighter2 = data["participant2Name"]

# Parse moneylines from each bookmaker
for slug, book_data in data["bookmakerOdds"].items():
    markets = book_data.get("markets", {})

    # Market 201 = Winner (moneyline)
    if "201" not in markets:
        continue

    outcomes = markets["201"].get("outcomes", {})

    # Fighter 1 price: outcome 201 in market 201
    f1_price = None
    if "201" in outcomes:
        player = outcomes["201"].get("players", {}).get("0", {})
        f1_price = player.get("price")

    # Fighter 2 price: outcome 202 in market 201
    # Some bookmakers put Fighter 2 in market 241 / outcome 242 instead
    f2_price = None
    if "202" in outcomes:
        player = outcomes["202"].get("players", {}).get("0", {})
        f2_price = player.get("price")
    elif "241" in markets:
        m241 = markets["241"].get("outcomes", {})
        if "242" in m241:
            player = m241["242"].get("players", {}).get("0", {})
            f2_price = player.get("price")

    if f1_price and f2_price:
        print(f"{slug:<25} {fighter1}: {f1_price:<8} {fighter2}: {f2_price}")

Watch out: Some bookmakers split the moneyline across two market IDs. Fighter 1 is always outcome 201 in market 201. Fighter 2 is usually outcome 202 in market 201, but about half the bookmakers put it in market 241 / outcome 242 instead. The code above handles both patterns.

Step 4: Build a Cross-Book Comparison

This is where it gets useful. Pull every bookmaker’s price, calculate the margin (overround), and find the best line for each fighter.


import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

fixture_id = "YOUR_FIXTURE_ID"

r = requests.get(f"{BASE_URL}/odds", params={
    "apiKey": API_KEY,
    "fixtureId": fixture_id
})
data = r.json()

fighter1 = data["participant1Name"]
fighter2 = data["participant2Name"]

rows = []
for slug, book_data in data["bookmakerOdds"].items():
    markets = book_data.get("markets", {})
    if "201" not in markets:
        continue

    outcomes = markets["201"].get("outcomes", {})

    f1 = outcomes.get("201", {}).get("players", {}).get("0", {})
    f1_price = f1.get("price") if isinstance(f1, dict) else None

    f2 = outcomes.get("202", {}).get("players", {}).get("0", {})
    f2_price = f2.get("price") if isinstance(f2, dict) else None

    # Check market 241 for Fighter 2 if not in 201
    if not f2_price and "241" in markets:
        f2_alt = markets["241"].get("outcomes", {}).get("242", {})
        f2_alt = f2_alt.get("players", {}).get("0", {})
        f2_price = f2_alt.get("price") if isinstance(f2_alt, dict) else None

    if f1_price and f2_price:
        margin = (1/f1_price + 1/f2_price - 1) * 100
        rows.append({
            "book": slug,
            "f1": f1_price,
            "f2": f2_price,
            "margin": margin
        })

# Sort by tightest margin (best value)
rows.sort(key=lambda x: x["margin"])

print(f"{'Bookmaker':<25} {fighter1:<12} {fighter2:<12} {'Margin'}")
print("-" * 60)
for r in rows:
    print(f"{r['book']:<25} {r['f1']:<12} {r['f2']:<12} {r['margin']:.1f}%")

# Best available prices
best_f1 = max(rows, key=lambda x: x["f1"])
best_f2 = max(rows, key=lambda x: x["f2"])
print(f"\nBest {fighter1}: {best_f1['f1']} @ {best_f1['book']}")
print(f"Best {fighter2}: {best_f2['f2']} @ {best_f2['book']}")

# Check for arb opportunity
combined = 1/best_f1["f1"] + 1/best_f2["f2"]
if combined < 1:
    profit = (1/combined - 1) * 100
    print(f"\nARB DETECTED: {profit:.2f}% profit")
    print(f"  Back {fighter1} @ {best_f1['f1']} ({best_f1['book']})")
    print(f"  Back {fighter2} @ {best_f2['f2']} ({best_f2['book']})")
else:
    print(f"\nNo arb (combined implied prob: {combined:.3f})")

Step 5: Spot Value Against the Sharp Line

Pinnacle is the sharpest bookmaker in the world. Their odds reflect the true probability better than anyone else. If a soft bookmaker is offering better odds than Pinnacle on the same fighter, that’s a potential +EV bet.


import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

fixture_id = "YOUR_FIXTURE_ID"

r = requests.get(f"{BASE_URL}/odds", params={
    "apiKey": API_KEY,
    "fixtureId": fixture_id
})
data = r.json()

fighter1 = data["participant1Name"]
fighter2 = data["participant2Name"]

def get_moneyline(book_data):
    """Extract both fighter prices from a bookmaker."""
    markets = book_data.get("markets", {})
    if "201" not in markets:
        return None, None
    outcomes = markets["201"].get("outcomes", {})

    f1 = outcomes.get("201", {}).get("players", {}).get("0", {})
    p1 = f1.get("price") if isinstance(f1, dict) else None

    f2 = outcomes.get("202", {}).get("players", {}).get("0", {})
    p2 = f2.get("price") if isinstance(f2, dict) else None
    if not p2 and "241" in markets:
        f2_alt = markets["241"].get("outcomes", {}).get("242", {})
        f2_alt = f2_alt.get("players", {}).get("0", {})
        p2 = f2_alt.get("price") if isinstance(f2_alt, dict) else None

    return p1, p2

books = data["bookmakerOdds"]

# Get Pinnacle's line as the benchmark
if "pinnacle" not in books:
    print("Pinnacle not pricing this fixture")
    exit()

pin_f1, pin_f2 = get_moneyline(books["pinnacle"])
print(f"Pinnacle (sharp) line: {fighter1} {pin_f1} / {fighter2} {pin_f2}")
print(f"Pinnacle implied: {1/pin_f1:.1%} / {1/pin_f2:.1%}\n")

# Find books offering better than Pinnacle
print(f"Books beating Pinnacle on {fighter1} (>{pin_f1}):")
for slug, bdata in books.items():
    if slug == "pinnacle":
        continue
    p1, p2 = get_moneyline(bdata)
    if p1 and p1 > pin_f1:
        edge = (p1/pin_f1 - 1) * 100
        print(f"  {slug:<25} {p1:<8} (+{edge:.1f}% edge)")

print(f"\nBooks beating Pinnacle on {fighter2} (>{pin_f2}):")
for slug, bdata in books.items():
    if slug == "pinnacle":
        continue
    p1, p2 = get_moneyline(bdata)
    if p2 and p2 > pin_f2:
        edge = (p2/pin_f2 - 1) * 100
        print(f"  {slug:<25} {p2:<8} (+{edge:.1f}% edge)")

Any book offering odds above Pinnacle’s line is giving you a mathematical edge — assuming Pinnacle’s price reflects the true probability. This is the foundation of value betting, and OddsPapi makes it trivial to scan across 46 bookmakers in one call.

Step 6: Backtest with Historical Odds

OddsPapi includes free historical odds on every tier — no add-on fee. Use the /historical-odds endpoint to see how a fighter’s price moved pre-fight.


import requests
import time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

fixture_id = "YOUR_FIXTURE_ID"

# Historical endpoint: max 3 bookmakers per call
r = requests.get(f"{BASE_URL}/historical-odds", params={
    "apiKey": API_KEY,
    "fixtureId": fixture_id,
    "bookmakers": "pinnacle,bet365,draftkings"
})
data = r.json()

# historical-odds uses "bookmakers" (not "bookmakerOdds")
# players["0"] is a LIST of snapshots (not a single dict)
for slug, book_data in data.get("bookmakers", {}).items():
    markets = book_data.get("markets", {})
    if "201" not in markets:
        continue

    outcomes = markets["201"].get("outcomes", {})
    if "201" in outcomes:
        snapshots = outcomes["201"]["players"]["0"]  # This is a list
        print(f"{slug} — Fighter 1 price history ({len(snapshots)} snapshots):")
        for snap in snapshots[-5:]:  # Last 5 movements
            print(f"  {snap['createdAt'][:16]}  {snap['price']}")
        print()

Important: The historical endpoint has a different response shape than the live endpoint. The top-level key is bookmakers (not bookmakerOdds), and players["0"] is a list of price snapshots, not a single dict. Max 3 bookmakers per call — loop with different combinations if you need more.

What Markets Are Available for MMA?

Most bookmakers price MMA with a single market: moneyline (Winner). Unlike soccer or basketball, you won’t find hundreds of props across every book. Here’s what’s available:

Market Market ID Outcome IDs Coverage
Winner (Moneyline) 201 201 (Fighter 1), 202 (Fighter 2) All 46 bookmakers
Winner (alt) 241 242 (Fighter 2) ~25 bookmakers (supplements market 201)

Don’t hardcode market catalogs — use /v4/markets?sportId=20 to pull the full list dynamically. As bookmakers add round props and method-of-victory markets, they’ll show up automatically.

Full Script: UFC Odds Scanner

Here’s the complete script that pulls every upcoming UFC fight and compares moneylines across all available bookmakers:


import requests
import time
from datetime import datetime, timedelta

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

def get_moneyline(book_data):
    """Extract fighter prices from a bookmaker."""
    markets = book_data.get("markets", {})
    if "201" not in markets:
        return None, None
    outcomes = markets["201"].get("outcomes", {})

    f1 = outcomes.get("201", {}).get("players", {}).get("0", {})
    p1 = f1.get("price") if isinstance(f1, dict) else None

    f2 = outcomes.get("202", {}).get("players", {}).get("0", {})
    p2 = f2.get("price") if isinstance(f2, dict) else None
    if not p2 and "241" in markets:
        f2_alt = markets["241"].get("outcomes", {}).get("242", {})
        f2_alt = f2_alt.get("players", {}).get("0", {})
        p2 = f2_alt.get("price") if isinstance(f2_alt, dict) else None

    return p1, p2

# 1. Get upcoming UFC fixtures
today = datetime.utcnow().strftime("%Y-%m-%d")
end = (datetime.utcnow() + timedelta(days=10)).strftime("%Y-%m-%d")

r = requests.get(f"{BASE_URL}/fixtures", params={
    "apiKey": API_KEY, "sportId": 20, "from": today, "to": end
})
fixtures = [f for f in r.json() if f.get("hasOdds")]
print(f"Found {len(fixtures)} UFC fixtures with odds\n")

# 2. Scan each fight
for fix in fixtures:
    time.sleep(1)  # Respect rate limits

    f1_name = fix["participant1Name"]
    f2_name = fix["participant2Name"]
    card = fix.get("tournamentName", "Unknown Card")

    r = requests.get(f"{BASE_URL}/odds", params={
        "apiKey": API_KEY, "fixtureId": fix["fixtureId"]
    })
    odds = r.json()

    rows = []
    for slug, bdata in odds.get("bookmakerOdds", {}).items():
        p1, p2 = get_moneyline(bdata)
        if p1 and p2:
            rows.append({"book": slug, "f1": p1, "f2": p2})

    if not rows:
        continue

    best_f1 = max(rows, key=lambda x: x["f1"])
    best_f2 = max(rows, key=lambda x: x["f2"])
    combined = 1/best_f1["f1"] + 1/best_f2["f2"]

    print(f"{f1_name} vs {f2_name} ({card})")
    print(f"  {len(rows)} bookmakers | Best {f1_name}: {best_f1['f1']} @ {best_f1['book']}")
    print(f"  Best {f2_name}: {best_f2['f2']} @ {best_f2['book']}")

    if combined < 1:
        profit = (1/combined - 1) * 100
        print(f"  >>> ARB: {profit:.2f}% profit")
    print()

What’s Next

You’ve got live moneylines from 46 bookmakers, cross-book comparison, and value detection against the Pinnacle sharp line. From here you can:

  • Build an arb scanner — automate the cross-book comparison and alert on profitable opportunities. See our arb bot tutorial.
  • Track line movement — use the historical odds endpoint to chart how MMA lines move from open to close.
  • Add WebSocket feeds — get real-time odds pushes instead of polling. See our WebSocket guide.
  • Backtest MMA models — historical data is free on every tier. See our backtesting guide.

Get your free API key and start pulling UFC odds in 5 minutes. No credit card. No scraping. 46 bookmakers in one call.