Free Odds API: 250 Requests, 274 Bookmakers in One Call

Free Odds API - OddsPapi API Blog
How To Guides January 31, 2026

You Need Data Before You Can Afford Data

You want to build a betting model, an arbitrage scanner or a value finder. First you need odds to prove the idea works, and most feeds want money before you write a line of code.

OddsPapi’s free plan gives you every sport and every bookmaker for 250 requests a month, with no credit card. The number that matters is what one request returns. On 29 September 2026 we sent one /odds call for Spain v Croatia in the UEFA Nations League. It came back with 274 bookmakers, 871 market IDs and 194,745 prices.

This guide measures that board, shows you how to spend 250 requests without waste, and gives you five tested Python scripts: live odds, sharp versus soft prices, and free opening and closing lines from history.

What the Free Plan Includes

The sign-up page states the plan in one line: all sports and all bookmakers, 250 requests a month. Here is what that covers, measured on 29 September 2026.

Feature Free plan
Bookmakers All of them: 361 slugs in /v4/bookmakers, 286 flagged liveOdds
Sports All 69 in /v4/sports
Sharp books and exchanges Pinnacle, Singbet, Betfair Exchange, Polymarket and Kalshi all priced Spain v Croatia
Soft books bet365, DraftKings, FanDuel and 1xBet all priced Spain v Croatia
Historical odds Included. Full snapshot history per fixture, up to 3 bookmakers per call
Requests 250 a month
Credit card Not needed

250 Requests vs 500 Credits

The Odds API’s free Starter plan gives you 500 credits a month. The count looks bigger until you read how their docs charge a call. A live odds call costs 1 credit per region per market. A historical call costs 10 credits per region per market. Their pricing page lists “most bookmakers” on the free plan. For the full breakdown, read The Odds API free tier limits.

One call for… OddsPapi free The Odds API free
Moneyline, spreads and totals, US + UK + EU regions 1 request 9 credits (3 markets x 3 regions)
Every market a fixture carries (871 on Spain v Croatia) 1 request 1 credit per market per region
Historical 1X2 for one fixture, full snapshot history 1 request (3 books) 10 credits per region, one snapshot per call
Monthly allowance 250 requests 500 credits

The Odds API rates are from their v4 docs and pricing page, read on 29 September 2026. If you need a wider comparison of paid tiers, see Odds API pricing compared. For every free plan and trial in one list, see the free odds API roundup on SportsAPIs.dev.

Get Started: Python Tutorial

Step 1: Get Your Free API Key

Sign up at oddspapi.io. You get a key at once. OddsPapi reads the key from the apiKey query parameter, not from a header.

Step 2: Verify Your Key

import requests

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

response = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
print(f"Status: {response.status_code}")
print(f"Sports available: {len(response.json())}")

Output on 29 September 2026:

Status: 200
Sports available: 69

Step 3: Browse Sports

import requests

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

response = requests.get(f"{BASE_URL}/sports", params={"apiKey": API_KEY})
for sport in response.json()[:10]:
    print(f"{sport['sportId']}: {sport['sportName']}")
10: Soccer
11: Basketball
12: Tennis
13: Baseball
14: American Football
15: Ice Hockey
16: ESport Dota
17: ESport Counter-Strike
18: ESport League of Legends
19: Darts

Cache this list and the bookmaker catalogue. They change rarely, and every call you skip is a call you keep for odds.

Step 4: Find Tournaments with Fixtures

import requests

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

response = requests.get(f"{BASE_URL}/tournaments", params={
    "apiKey": API_KEY,
    "sportId": 10,  # Soccer
})
tournaments = response.json()

# futureFixtures > 0 means the tournament has fixtures on the calendar
active = [t for t in tournaments if t.get("futureFixtures", 0) > 0]
print(f"Soccer tournaments: {len(tournaments)}, with future fixtures: {len(active)}")

for t in sorted(active, key=lambda t: -t["futureFixtures"])[:5]:
    print(f"  {t['tournamentId']:>6}  {t['categoryName']} / {t['tournamentName']}: {t['futureFixtures']}")
Soccer tournaments: 1967, with future fixtures: 607
     544  Spain / Segunda Federacion: 1605
   48343  Spain / U19 Division de Honor Juvenil: 1567
   46053  Greece / Gamma Ethniki: 1168
   26178  USA / NCAA Regular Season, Women: 999
   26176  USA / NCAA, Regular Season: 934

Filter on futureFixtures, not upcomingFixtures. On 29 September the Premier League showed 330 future fixtures and 0 upcoming, so an upcomingFixtures filter drops it.

Step 5: Get Fixtures for a Tournament

import requests
from datetime import date, timedelta

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

TOURNAMENT_ID = 23755  # UEFA Nations League; 17 = Premier League, 8 = LaLiga

today = date.today()
response = requests.get(f"{BASE_URL}/fixtures", params={
    "apiKey": API_KEY,
    "tournamentId": TOURNAMENT_ID,
    "from": today.isoformat(),
    "to": (today + timedelta(days=10)).isoformat(),
})

# A 404 means an empty window (off-season or international break), not an error
fixtures = response.json() if response.status_code == 200 else []
with_odds = sorted((f for f in fixtures if f.get("hasOdds")), key=lambda f: f["startTime"])
print(f"Fixtures: {len(fixtures)}, with odds: {len(with_odds)}")

for f in with_odds[:3]:
    print(f"  {f['fixtureId']}  {f['participant1Name']} v {f['participant2Name']}  {f['startTime']}")
Fixtures: 62, with odds: 62
  id1002375568932360  Moldova v Faroe Islands  2026-09-29T16:00:00.000Z
  id1002375568932506  Finland v Belarus  2026-09-29T16:00:00.000Z
  id1002375568931694  Czechia v England  2026-09-29T18:45:00.000Z

If a tournament has no fixtures in the window, /fixtures returns 404. On 29 September, during the international break, LaLiga and the Champions League both returned FIXTURE_NOT_FOUND for the next ten days. The script treats 404 as an empty list.

Step 6: One Request, the Whole Board

This script spends one request on one fixture and counts what comes back. Then it pulls the 1X2 (market 101) from three sharp or exchange prices and three soft books.

import requests

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

FIXTURE_ID = "id1002375568931696"  # Spain v Croatia, from Step 5

response = requests.get(f"{BASE_URL}/odds", params={"apiKey": API_KEY, "fixtureId": FIXTURE_ID})
response.raise_for_status()
books = response.json()["bookmakerOdds"]

# Drop internal test feeds before you count anything
books = {s: b for s, b in books.items() if not s.startswith("pinnacle+") and s != "demo"}

market_ids, prices, active = set(), 0, 0
for b in books.values():
    for mid, market in (b.get("markets") or {}).items():
        market_ids.add(mid)
        for outcome in market["outcomes"].values():
            for p in outcome["players"].values():
                prices += 1
                active += bool(p.get("active"))

print(f"One request: {len(books)} bookmakers, {len(market_ids)} market IDs, "
      f"{prices:,} prices ({active:,} active), {len(response.content) / 1e6:.1f} MB")

# Market 101 = Full Time Result (1X2); outcomes 101 home, 102 draw, 103 away
def one_x_two(slug):
    m = (books.get(slug, {}).get("markets") or {}).get("101")
    if not m:
        return None
    legs = [m["outcomes"].get(k, {}).get("players", {}).get("0", {}) for k in ("101", "102", "103")]
    if not all(p.get("active") for p in legs):
        return None
    return tuple(p["price"] for p in legs)

quotes = {s: q for s in books if (q := one_x_two(s))}
print(f"Active 1X2 quotes: {len(quotes)} books, {len(set(quotes.values()))} distinct price tuples")

for slug in ["pinnacle", "singbet", "betfair-ex", "polymarket", "bet365", "draftkings"]:
    q = quotes.get(slug)
    if q:
        margin = sum(1 / x for x in q) - 1
        print(f"  {slug:<11} {q[0]:>6} {q[1]:>6} {q[2]:>6}   margin {margin:.2%}")

Output, 29 September 2026 at 13:15 UTC, five and a half hours before kick-off:

One request: 274 bookmakers, 871 market IDs, 194,745 prices (153,280 active), 65.4 MB
Active 1X2 quotes: 264 books, 98 distinct price tuples
  pinnacle     1.202   7.45  12.97   margin 4.33%
  singbet       1.21    6.8   14.0   margin 4.49%
  betfair-ex    1.22    8.0   16.5   margin 0.53%
  polymarket    1.22  7.692 14.286   margin 1.97%
  bet365        1.19    6.5   11.0   margin 8.51%
  draftkings     1.2    6.5   11.0   margin 7.81%

Three findings come out of that one request:

  • Dedupe before you count books. 264 books quoted an active 1X2, but they quoted only 98 distinct price tuples. Many slugs copy another book’s prices. The consensus odds guide shows the dedupe step.
  • The soft books pay less on the long shot. bet365 and DraftKings both quoted Croatia at 11.0. Pinnacle had 12.97 and Betfair Exchange had 16.5. That gap is the raw material for line shopping.
  • Read the active flag. 41,465 of the 194,745 prices were suspended. If you skip the flag, your averages include dead prices.

The payload is large: 65.4 MB for this fixture. If you only need a few books, pass bookmakers=pinnacle,bet365 on the same call.

Step 7: Free Opening and Closing Lines

Historical odds are on the free plan. One call returns every snapshot for up to three bookmakers on a played fixture. This script reads the opening and closing 1X2 for Brighton v Arsenal (Premier League, 19 September 2026).

import requests

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

FIXTURE_ID = "id1000001772221276"  # Brighton v Arsenal, played 19 Sep 2026
KICK_OFF = "2026-09-19T14:00:00.000Z"  # startTime from /fixtures

# /historical-odds takes at most 3 bookmakers per call
response = requests.get(f"{BASE_URL}/historical-odds", params={
    "apiKey": API_KEY,
    "fixtureId": FIXTURE_ID,
    "bookmakers": "pinnacle,bet365,draftkings",
})
response.raise_for_status()

for slug, book in response.json()["bookmakers"].items():
    outcomes = book["markets"]["101"]["outcomes"]
    opening, closing = [], []
    for leg in ("101", "102", "103"):
        # players["0"] is a list of snapshots, oldest first
        snaps = [s for s in outcomes[leg]["players"]["0"]
                 if s["active"] and s["createdAt"] < KICK_OFF]
        opening.append(snaps[0]["price"])
        closing.append(snaps[-1]["price"])
    first_seen = min(s["createdAt"] for leg in outcomes.values() for s in leg["players"]["0"])
    print(f"{slug:<11} first quote {first_seen[:16]}")
    for label, q in (("open", opening), ("close", closing)):
        print(f"  {label:<5} {q[0]:>6} {q[1]:>6} {q[2]:>6}   margin {sum(1 / x for x in q) - 1:.2%}")
bet365      first quote 2026-09-05T19:34
  open   4.333    3.6   1.71   margin 9.34%
  close    5.0    3.8   1.66   margin 6.56%
draftkings  first quote 2026-09-07T01:13
  open     4.0   3.75    1.8   margin 7.22%
  close    5.0   3.85   1.69   margin 5.15%
pinnacle    first quote 2026-09-06T19:16
  open    3.48   3.51   2.04   margin 6.25%
  close    5.3    4.0  1.689   margin 3.07%

Pinnacle opened Arsenal at 2.04 thirteen days out and closed at 1.689. Its margin fell from 6.25% to 3.07% on the way. A bettor who took 2.04 at the open held a price 20.8% above the close. That gap is closing line value (CLV), the number most models test against. The backtesting guide builds a full test on this endpoint.

The live and historical payloads differ. Live odds nest under bookmakerOdds and players["0"] is one price object. Historical odds nest under bookmakers and players["0"] is a list of snapshots.

How Far 250 Requests Go

Every call is one request, whatever the payload size. This is how the jobs above price out:

Job Requests
Discovery: /sports, /bookmakers, /tournaments (cache them) 3
A six-fixture Nations League evening: 1 /fixtures + 6 /odds 7
A ten-fixture Premier League matchday before kick-off 11
Opening and closing lines for 3 books on one played fixture 1
A 200-fixture closing-line backtest, 3 books each 200

Two habits stretch the budget. Pull odds once per fixture close to kick-off instead of polling. Space calls to the same endpoint by at least one second, because the API rate-limits per endpoint and returns HTTP 429 with a retryMs field. A 429 body is valid JSON, so check the status code before you read bookmakerOdds.

When to Upgrade

  • You poll the same fixtures all day. Polling burns the 250 fast. Paid plans price per bookmaker, per sport and per request volume on the pricing calculator.
  • You need push updates. WebSocket streaming is not part of the free plan. The WebSocket odds guide covers what it adds.
  • You run a production system. Use the free plan to prove the model on real prices first.

FAQ

Is there a free odds API with Pinnacle?

Yes. OddsPapi’s free plan covers every bookmaker, Pinnacle included. On 29 September 2026 one call on Spain v Croatia returned Pinnacle’s 1X2 at 1.202 / 7.45 / 12.97 with a stake limit on each leg.

How many requests does the OddsPapi free plan include?

250 a month, across all sports and all bookmakers. Each call counts as one request, whether it returns one book or 274.

Does the free plan include historical odds?

Yes. /historical-odds returns every snapshot for up to three bookmakers per call. The window is rolling: in September 2026 it reached back about eight months on the Premier League.

How many bookmakers does one call return?

It depends on the fixture. Six Nations League fixtures on 29 September 2026 returned between 216 and 274 bookmakers each. Dedupe on the price tuple before you count, because regional skins quote identical prices.

Is OddsPapi free forever or a trial?

It is a plan, not a trial. The sign-up form asks for an email and a password, not a card, and the plan carries 250 requests a month.

How does the free plan compare to The Odds API free tier?

The Odds API gives 500 credits and charges 1 credit per region per market on a live call, 10 on a historical call. OddsPapi gives 250 requests and charges 1 per call, whatever it returns.

Stop Scraping. Start Building.

A scraper breaks every time a bookmaker changes its page. One free key gets you 350+ bookmakers, 69 sports and history under one schema.

Get your free OddsPapi API key