College Football Odds API: One Field Finds Every Priced Board
College football Week 1 puts 136 NCAA games on the board between Thursday and Monday, 67 of them on Saturday alone. The ones the market prices are deep: a median of 107 bookmakers per fixture, topping out at 145, with full spread, total and player-prop menus behind them.
The catch is that those 136 fixtures include the Division II and III slate: Barton at Virginia State, Wingate at North Carolina-Pembroke, Kentucky Wesleyan at Kentucky Christian. No sportsbook prices those games, so 58 of the 136 come back with an empty board. That is the market, not the feed. If no book has opened a line, there is no line to serve.
So the job is to tell the two groups apart before you spend the calls. One field in the /fixtures response does it, and it cuts 136 /odds calls down to 54 with no loss. This post shows the filter, then what a 145-book college board actually contains. All measured live, on the free tier.
The distribution is not a bell curve, it is a cliff
Books per fixture across all 136 Week 1 games:
| Bookmakers on the fixture | Fixtures | Share |
|---|---|---|
| 0 | 58 | 42.6% |
| 1 to 9 | 24 | 17.6% |
| 10 to 49 | 0 | 0% |
| 50 to 99 | 5 | 3.7% |
| 100+ | 49 | 36.0% |
Nothing sits between 10 and 49 books. A college football fixture is either fully covered or barely covered, and the median of 2.0 books per fixture describes no actual game on the slate. If you are reporting a coverage average for this sport, you are reporting a number that does not exist.
The empty half is the small-college slate: Barton at Virginia State, Wingate at North Carolina-Pembroke, Marian at Indianapolis, Kentucky Wesleyan at Kentucky Christian. Real fixtures with real kick-off times that no sportsbook prices.
The one-call fix: read externalProviders
Every fixture from /fixtures carries an externalProviders object mapping it to third-party data vendors. On college football, the number of keys in it predicts the board almost perfectly.
| Providers on the fixture | Fixtures | Median books | Empty boards |
|---|---|---|---|
1 (betradarId only) |
82 | 0 | 58 (and the rest max out at 2) |
| 3 | 9 | 106 | 0 |
| 4 | 44 | 107 | 0 |
| 5 | 1 | 141 | 0 |
All 54 fixtures with two or more providers have a real board. Not one exception. Every fixture carrying only betradarId tops out at two bookmakers. So one filter on the fixtures response cuts 136 /odds calls down to 54, keeps 100% of the real boards, and costs you nothing but a handful of two-book fixtures.
import requests, time
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def get(path, **params):
for _ in range(5):
params["apiKey"] = API_KEY
r = requests.get(f"{BASE_URL}/{path}", params=params)
if r.status_code == 429:
time.sleep(r.json()["error"].get("retryMs", 1500) / 1000 + 1.2)
continue
return r.status_code, r.json()
return r.status_code, {}
# NCAA regular season is tournamentId 27653 under sportId 14.
# `to` is a midnight-UTC instant: use the day AFTER your last day.
status, fixtures = get("fixtures", sportId=14, tournamentId=27653,
**{"from": "2026-08-27", "to": "2026-09-02"})
def likely_covered(f, min_providers=2):
providers = {k: v for k, v in (f.get("externalProviders") or {}).items() if v}
return len(providers) >= min_providers
worth_calling = [f for f in fixtures if likely_covered(f)]
print(len(fixtures), "fixtures ->", len(worth_calling), "worth an /odds call")
# 136 fixtures -> 54 worth an /odds call
Note the null-check on the values. The field is always present; what varies is how many of its keys are populated.
The caveat that keeps this honest
This works because externalProviders is sparse on college football. It is useless on a sport where the field is full. On a Grand Slam tennis draw, all ten provider IDs populate on every row, including bracket placeholders with no players in them yet, so the field carries no signal at all.
Check the shape before you trust it. One line tells you which regime you are in:
import collections
spread = collections.Counter(
len({k: v for k, v in (f.get("externalProviders") or {}).items() if v})
for f in fixtures
)
print(sorted(spread.items()))
# [(1, 82), (3, 9), (4, 44), (5, 1)] -> sparse, so it predicts
# [(10, 253)] -> saturated, so it does not
pinnacleId is a near-perfect predictor of the sharp line
The same field carries a second, sharper signal. pinnacleId appears on 8 of the 136 fixtures. Pinnacle actually priced 6 of those 8, and 0 of the other 128.
Circa Sports tracks it exactly: the same six fixtures, no others. So if you need a sharp benchmark for a college game, pinnacleId tells you whether one can exist before you spend the call. Six fixtures out of 136 is the honest size of the sharp college board in Week 1.
sharp_candidates = [f for f in fixtures
if (f.get("externalProviders") or {}).get("pinnacleId")]
print(len(sharp_candidates), "fixtures where a Pinnacle line is even possible") # 8
Prediction markets go where sportsbooks will not
Those 24 fixtures with exactly two bookmakers are the interesting tail. The two books are the same every time: kalshi and polymarket.us. No sportsbook at all.
Across the whole 136-game slate, presence runs:
| Venue | Fixtures priced |
|---|---|
kalshi |
73 of 136 |
bet365 |
52 |
draftkings |
48 |
pinnacle |
6 |
circasports |
6 |
polymarket |
0 |
Kalshi covers more college football than any sportsbook on the board, and it is the only venue quoting Division II football at all. Note the split between polymarket and polymarket.us: the international slug is on none of the 136 and the US slug is on the long tail. They are different feeds, so query the one you mean.
What a covered game actually looks like
The deepest board of the weekend is North Carolina at TCU: 145 bookmakers, 1,778 distinct market IDs. Two filters before you count anything.
First, drop the internal test feeds. pinnacle+30 and pinnacle+5 quote the identical number to pinnacle, so an unfiltered count includes the sharp three times.
Second, dedupe on the price tuple. Several distinct slugs ship byte-identical prices while the catalogue reports cloneOf: null. Across all 136 fixtures, 5,191 complete quotes collapse to 1,939 independent prices, a 62.6% collapse. On the TCU game, 129 usable quotes become 58.
def moneylines(fixture_id):
"""{slug: (home_price, away_price)} for active, two-sided quotes only."""
status, odds = get("odds", fixtureId=fixture_id)
out = {}
for slug, book in odds.get("bookmakerOdds", {}).items():
if slug.startswith("pinnacle+") or slug == "demo":
continue
market = book.get("markets", {}).get("141") # Winner (incl. overtime)
if not market:
continue
legs = {}
for outcome_id, outcome in market["outcomes"].items():
price = outcome["players"].get("0") # game lines key on "0"
if price and price.get("active") and price.get("price"):
legs[outcome_id] = price["price"]
if len(legs) == 2:
out[slug] = (legs["141"], legs["142"])
return out
quotes = moneylines("id1402765370894628") # North Carolina @ TCU
print(len(quotes), "quotes ->", len(set(quotes.values())), "independent")
# 129 quotes -> 58 independent (live counts drift between calls)
Margin ranking on that fixture, 129 ranked books:
| Rank | Book | Margin |
|---|---|---|
| 1 | polymarket.us |
0.52% |
| 2 | novig.us |
0.98% |
| 3 | kalshi |
1.02% |
| 4 | prophetx |
1.22% |
| 9 | circasports |
3.60% |
| 17 | pinnacle |
4.19% |
| 129 | sesamesport.bg |
11.29% |
Sixteen books beat Pinnacle on price here, and most of them are exchanges or prediction markets. That ranking says nothing about how much money sits behind each quote — polymarket.us in particular posts the tightest number on the board and frequently has nothing behind it, so read the exchangeMeta ladder before treating a tight quote as a benchmark.
Two-thirds of the board is switched off
Across the 136 fixtures, 471,588 prices and only 31.3% active. On the TCU game specifically it is 35.6%.
Books load a full menu early and suspend most of it. The flag that matters lives on the price object at players["0"], one level below the outcome. Do not use marketActive, suspended or bookmakerIsActive — all three disagree with their own prices often enough to break a parser.
This is the same effect as the board-width rule elsewhere: a wider board contains proportionally fewer live prices. Filter first, count second.
Old way vs OddsPapi
| Scraping / generic sports APIs | OddsPapi | |
|---|---|---|
| Week 1 slate | Schedule from one source, odds from another | One call, 136 fixtures, one ID space |
| Knowing which games have odds | Try them all | externalProviders filter, 136 → 54 |
| Board depth | One scraper per book | Up to 145 books in one JSON call |
| Sharp benchmark | Pinnacle account required | pinnacle and circasports in the payload |
| Vendor cross-mapping | Buy a mapping product | Betradar, Genius, LSports, OpticOdds IDs included |
A working Saturday pull
- Pull the week from
/fixtureswithtournamentId=27653. Remember an empty window returns HTTP 404, not an empty array. - Keep fixtures with two or more populated
externalProviderskeys. That is your real slate. - Flag the
pinnacleIdsubset separately — those are the only games where a sharp line can exist. - Call
/oddson the survivors withtime.sleep(1.0)between calls. Do not parallelise; at any worker count almost everything comes back 429. - Drop
pinnacle+anddemoslugs, filter on price-levelactive, then dedupe on the price tuple before you average anything.
Where to go next
- College football odds API — the fundamentals: discovery, markets, and the suspended-board problem.
- College football player props API — the 22 prop families that arrived on this slate.
- When do NFL odds open? — the same measurement run across a full NFL season.
- NFL alternate lines API — what those 1,778 market IDs contain.
- NFL prediction market liquidity — why a 0.52% quote can be unfillable.
- Fixture mapping with Betradar and Genius IDs — the other use for
externalProviders. - Circa Sports API — the book that tracks Pinnacle’s college map exactly.
- Line shopping in Python — best price across 145 books.
- Consensus odds — what to do once you have 57 independent prices.
- Free odds API — the tier all of this runs on.
Spend your calls where the board is
College football is the widest schedule in American sport and the most uneven board in it. The difference between 136 calls and 54 is one filter on a field you already have, and the difference between 145 books and 57 real prices is one dedupe. Both are free, on 350+ bookmakers including Pinnacle, Circa, bet365, DraftKings and Kalshi, with free historical odds behind every fixture.
Get your free API key and filter this Saturday’s slate before kick-off.