Tutorial · 12 min · endpoints verified 2026-09-11
NFL betting model API tutorial
Build a betting model the way a book-watcher actually works: pull the current consensus line for a game, then pull every snapshot behind it to see whether the number moved toward you or away from you before kickoff. About fifteen minutes.
Why this is hard
The problem with NFL betting data
A single sportsbook’s line tells you what one book thinks. It does not tell you what the market thinks, and it does not tell you whether the line you would get today is better or worse than the line that existed an hour ago. Both of those need aggregation across books and a snapshot history, which is the part most free feeds do not carry.
- A one-book price is noise. This build aggregates across dozens of sportsbooks per market — the sample game below carries 80 books on the moneyline alone — so the number reflects the market, not one outlier.
- Line movement is the actual signal. A closing line that moved 1.5 points toward one side between the open and kickoff is informative; the closing number alone is not.
- Moneyline, spread and totals are usually three different data shapes from three different vendors if you assemble this yourself. Here they are one response, one call.
- This is priced data. Bookmaker odds sit behind Edge on this API — the endpoints below are the two paid calls this build needs, and there are no free substitutes for them.
Path 1 · recommended
Have your AI agent build it
The player- and match-lookup half of this build is fully agentic; the odds themselves are not — no MCP tool serves bookmaker lines today. Use the server to resolve the game and pull context, then have your agent hit the two odds endpoints directly over HTTP with the same key.
1. Connect the MCP server
Add the server in your MCP client and sign in with your Big Balls account. Your client registers itself and handles the token exchange — there is no key to copy.
{
"mcpServers": {
"bigballs-sports-data": {
"url": "https://mcp.bigballsdata.com/mcp"
}
}
}2. Tools your agent gets
get_matchesLive, upcoming or historical matches for a sport or league.
get_coverageMachine-readable map of what we hold and what we do not.
3. Ask for what you want
Paste this at your agent. It is written to make the model check coverage before it designs anything, which is what stops it inventing a field we do not serve.
Build me a simple NFL closing-line model.
Use the Big Balls Sports Data MCP server to call get_matches for
american_football / nfl and find a game I name, so I have its match id.
The MCP server has no odds tool, so for the odds themselves call the REST
API directly with my key:
1. GET /v1/matches/{id}/odds — the current aggregated moneyline, spread and
total, with book_count per market.
2. GET /v1/odds/historical?match_id={id} — every snapshot (opening, t24h,
closing) so I can see whether the number moved and which way.
Compute, per market: how far the closing line moved from the opening line,
and which side of the move I'd be on if I bet the opener versus the close.
Tell me which numbers came from which call.What the agent cannot reach
Odds require an Edge-plan key — both calls above 403 on Free, Solo and Trio, and the error body names the plan and links straight to billing, so an agent reading the response can surface that rather than silently failing. Nothing else in this build is gated: match lookup and coverage are free.
Path 2 · hand-coded
Build it yourself
Every call below was run against production on 2026-09-11 with an Edge-tier key, against a live Rams–49ers game. The odds calls need Edge; everything else is free.
- 01
Find a game with odds
Filter matches to live or upcoming and check has_odds before spending an odds call on a game that has none.
GET /v1/matchesbashcurl -s "https://api.bigballsdata.com/v1/matches?sport=american_football&league=nfl&status=live" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668", "sport": "american_football", "league": "NFL", "home": { "id": "a6a6cdd2-b5fe-4bb9-bf37-a89daa891e5c", "name": "Los Angeles Rams", "short_name": "LAR" }, "away": { "id": "3393aa2f-1261-465e-bfe6-a5de7685c0f1", "name": "San Francisco 49ers", "short_name": "SF" }, "kickoff_utc": "2026-09-11T00:35:00.000Z", "status": "live", "score": { "home": 7, "away": 10 }, "has_odds": true } ] } - 02
Pull the current consensus line
One call returns moneyline, spread and total, each aggregated across every book currently quoting the game — book_count tells you how many. This game had 80 books on the moneyline and 114 on the total.
GET /v1/matches/:id/odds (Edge plan)bashcurl -s "https://api.bigballsdata.com/v1/matches/5b2e8a2c-9c4f-4f0f-b035-fa23b0311668/odds" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": { "odds": [ { "market": "moneyline", "participant": "Los Angeles Rams", "decimal_odds": 1.17, "book_count": 80 }, { "market": "moneyline", "participant": "San Francisco 49ers", "decimal_odds": 1.639, "book_count": 69 }, { "market": "spread", "participant": "Los Angeles Rams", "decimal_odds": 1.019, "book_count": 56 }, { "market": "total", "participant": "Over", "decimal_odds": 1.406, "line": 48.083, "book_count": 114 }, { "market": "total", "participant": "Under", "decimal_odds": 1.485, "line": 48.083, "book_count": 93 }, { "market": "handicap", "participant": "San Francisco 49ers", "decimal_odds": 1.934, "line": 3.655, "book_count": 29 }, { "market": "handicap", "participant": "Los Angeles Rams", "decimal_odds": 1.926, "line": -3.625, "book_count": 24 } ] }, "meta": { "source": "stored", "count": 8 } } - 03
Pull every snapshot for line movement
The historical endpoint returns every snapshot — opening, t24h and closing — newest first, each with its own sportsbook, price and line. This is what a single "current odds" call can never show you: which way the market moved before kickoff.
GET /v1/odds/historical (Edge plan)bashcurl -s "https://api.bigballsdata.com/v1/odds/historical?match_id=5b2e8a2c-9c4f-4f0f-b035-fa23b0311668" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "match_id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668", "snapshot_type": "closing", "snapshot_at": "2026-09-11T00:30:23.087Z", "market": "spread", "book": { "id": "oddsapi_betmgm", "name": "BetMGM" }, "line": { "price": 1.98, "value": -3.5, "participant": "Los Angeles Rams" }, "source": "aggregator-paid", "verification": "unverified", "verified_at": null } ] } - 04
Compute the move
Group snapshots by market and book, sort by snapshot_at, and diff the opening value against the closing one. A spread that opens at Rams -2.5 and closes at -3.5 moved a full point toward Los Angeles — the kind of signal a single-snapshot feed cannot show.
Line movement from snapshotspythonfrom collections import defaultdict def movement(snapshots, market): by_type = {s["snapshot_type"]: s for s in snapshots if s["market"] == market} opening = by_type.get("opening") closing = by_type.get("closing") if not opening or not closing: return None return { "book": closing["book"]["name"], "opening_line": opening["line"]["value"], "closing_line": closing["line"]["value"], "moved": round((closing["line"]["value"] or 0) - (opening["line"]["value"] or 0), 2), } - 05
Read each field honestly
verification is not decoration: closing-line rows can be "two_source_agreed", "two_source_diverged", "single_source" or "unverified" from real two-source cross-checking, never inferred from row-absence. A model that treats "unverified" as "wrong" will throw away most of its own history — treat it as "not yet cross-checked", not "bad".
Reference
Every endpoint this tutorial uses
All on the gateway at api.bigballsdata.com. Verified against production on 2026-09-11.
| Method | Path | Returns | Why you need it | Plan |
|---|---|---|---|---|
| GET | /v1/matches?sport=american_football&league=nfl&status= | Match id, teams, kickoff time, current score and has_odds | Finds the match id every odds call needs, and has_odds avoids spending a paid call on a game with none. | Free |
| GET | /v1/matches/:id/odds | Current moneyline, spread and total, aggregated across every quoting book | The consensus line the model is built on — book_count tells you how thin or deep the market is behind each number. | Edge |
| GET | /v1/odds/historical | Every snapshot (opening, t24h, closing) with per-book price and line | The only way to see movement. A single "current odds" call is a snapshot; this is the whole series. | Edge |
Pricing, honestly
Where the free tier stops
This build has no free-tier version worth shipping. Match lookup is free, but both odds calls 403 below Edge — the error body itself is where a free-tier reader learns that, and it names the tier and links straight to billing rather than returning a vague 403.
Free key
{
"error": {
"code": "forbidden",
"message": "Access to bookmaker odds requires the Edge plan or higher."
},
"suggested_fix": "Edge is $149/month and unlocks live and stored bookmaker lines across every book — moneyline, spread and totals — plus opening→closing movement and CLV.",
"docs_url": "https://bigballsdata.com/docs/endpoints#odds"
}Solo
{
"data": {
"odds": [
{ "market": "moneyline", "participant": "Los Angeles Rams", "decimal_odds": 1.17, "book_count": 80 }
]
},
"meta": { "source": "stored", "count": 8 }
}- Free and Solo cannot call either odds endpoint at all — this is a hard gate, not a truncated response.
- Edge ($149/month, per the API’s own upgrade message) unlocks both the current aggregated line and the full snapshot history behind it.
- The aggregation itself is the product: 80 books on one moneyline is not something a single-book feed can reconstruct after the fact.
More tutorials
Other build guides
Questions
- Can I get NFL odds on the free tier at all?
- No. Unlike most of this API, odds is an all-or-nothing gate: /v1/matches/:id/odds and /v1/odds/historical both return a 403 on Free, Solo and Trio, with no reduced-shape response below Edge. There is no free preview to build against.
- Whose odds are these — one sportsbook or many?
- Many, aggregated. The moneyline on the sample game above had 80 contributing books; the total had 114. book_count on every row tells you how many books fed that specific number, so you can weight thin markets differently from deep ones.
- What does "verification" mean on a historical snapshot?
- It is an evidence-based field from real two-source cross-checking — two_source_agreed, two_source_diverged (with divergence_categories), single_source, or unverified. A "verified" status is never inferred from a row simply being present; it means two independent sources were actually compared for that snapshot.
- Is there an MCP tool for odds?
- No. The live MCP server has no odds tool as of this writing — resolve the match through get_matches, then call the two REST odds endpoints directly with the same API key.
Related APIs
Other APIs you can build with: