Tutorial · 14 min · endpoints verified 2026-09-03
NFL fantasy football API tutorial
Build a fantasy football app the useful way: a PPR draft board that ranks receivers by floor and boom rate, not just by average — on real weekly scoring, the live 2026 depth chart and team Elo. About twenty minutes. Hand it to your agent, or write it yourself.
Why this is hard
The problem with fantasy football data
Every fantasy football tutorial stops in the same place: it fetches some numbers, prints a table, and leaves. The part that actually decides your draft — whether a player who averages 15 points scores 15 every week or scores 3 and 30 — needs per-week history that most sources make you scrape and reassemble yourself.
- Season averages hide the thing that loses leagues. Two receivers can average the same points and be completely different picks; the one with a 3-point floor sinks a week you cannot get back.
- The popular endpoints are undocumented. The best-known walkthroughs build on an unofficial fantasy-platform API, and they say so — one ends by admitting private-league auth is unsolved.
- Depth charts and scoring live in different places, so the question "is he still the WR1 on his own team" needs a second, usually scraped, source.
- Preseason and postseason weeks sit in the same feed as the regular season. Averaging them together quietly changes your rankings, and nothing warns you.
Path 1 · recommended
Have your AI agent build it
Start here. Our MCP server is live, and for this build the agentic path reaches everything the draft board needs — including the player-id lookup that is usually the tedious part. Point any MCP client at it and the model queries our data directly while it writes your code.
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
find_playersFind canonical player IDs by name for use with player-stat tools.
get_matchesLive, upcoming or historical matches for a sport or league.
get_player_statsPer-player career or per-season totals and per-game rates.
get_team_eloTeam Elo rating, rank and optional rating history.
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 PPR fantasy football draft board.
Use the Big Balls Sports Data MCP server. Call get_coverage for
american_football first, so you know what actually exists before designing
anything.
Then, for each receiver I name:
1. Resolve the name to an id with find_players. Confirm the team looks right
before using the id — some names return several candidates.
2. Call get_player_stats (sport: american_football) for the 2025 season.
3. Keep only rows where season_type is "REG". Postseason weeks are in the
same response and will skew the averages if you leave them in.
4. From the weekly fantasy_points_ppr values compute: points per game, a
floor (median of the worst quarter of weeks), boom rate (weeks >= 20) and
bust rate (weeks < 10).
5. Call get_team_elo for each player's team so schedule strength is visible
beside the production.
Rank by floor, not by average, and show both columns so I can see where they
disagree. Tell me which numbers came from which tool.What the agent cannot reach
One thing this build uses has no MCP tool: the depth chart. `get_player_stats` will tell you how a receiver scored, but not whether he is still WR1 on his own roster — for that an agent has to call GET /v1/nfl/depth-charts over plain HTTP with the same key. It is the same gateway and the same auth; it is just not in the tool catalogue. Ask your agent to fetch it directly rather than letting it infer a depth chart from usage, which it will otherwise happily do.
Path 2 · hand-coded
Build it yourself
Every call below was run against production on 2026-09-03 and the responses are trimmed copies of what came back. A free key is enough for all of it. No SDK, no build step — curl and about forty lines of Python.
- 01
Get a key and confirm it works
Create a free key in the dashboard and check that the gateway sees it. A 200 here means everything later is a data problem rather than an auth problem.
Verify your keybashcurl -s "https://api.bigballsdata.com/v1/coverage?sport=american_football" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": { "sports": [ { "slug": "american_football", "leagues": [ { "key": "nfl", "matches": { "total": 2026, "finished": 1754, "scheduled": 272 } }, { "key": "ncaaf", "matches": { "total": 9701, "finished": 8635, "scheduled": 1065 } }, { "key": "ncaaf-fcs", "matches": { "total": 6774, "finished": 6138, "scheduled": 636 } } ] } ] } } - 02
Turn a player name into an id
Everything else is keyed on a player id, so start here. Pass ?name= with a sport filter. Always filter: an unfiltered /v1/players returns all 66,115 players across every sport we hold, and the first rows are not who you want.
GET /v1/players?name=bashcurl -s "https://api.bigballsdata.com/v1/players?name=Justin%20Jefferson&sport=american_football" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "id": "b24cbd4f-32f7-431e-91fb-06394edecb6f", "name": "Justin Jefferson", "position": "Wide Receiver", "jersey_number": "18", "team_id": "5e2e55b4-ca5b-416c-909b-1fa2e7b9ed20", "team_name": "Minnesota Vikings", "league_name": "NFL", "sport": "american_football" } ] } - 03
Pull the weekly scoring, already in PPR
One call per player returns every week of the season with fantasy_points_ppr computed for you. This is the number the whole board is built on, and it is populated on every row — you are not deriving it from yardage and touchdowns yourself.
GET /v1/players/:id/statsbashcurl -s "https://api.bigballsdata.com/v1/players/b24cbd4f-32f7-431e-91fb-06394edecb6f/stats\ ?sport=american_football&season=2025" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "season": 2025, "week": 1, "season_type": "REG", "team": "MIN", "position": "WR", "receptions": 4, "targets": 7, "receiving_yards": 44, "receiving_tds": 1, "fantasy_points_ppr": 14.8 }, { "season": 2025, "week": 2, "season_type": "REG", "team": "MIN", "position": "WR", "receptions": 3, "targets": 6, "receiving_yards": 81, "receiving_tds": 0, "fantasy_points_ppr": 11.1 }, { "season": 2025, "week": 3, "season_type": "REG", "team": "MIN", "position": "WR", "receptions": 5, "targets": 7, "receiving_yards": 75, "receiving_tds": 0, "fantasy_points_ppr": 12.5 } ] } - 04
Drop the postseason weeks before you average anything
The response carries regular-season and postseason weeks together, marked by season_type. Leaving playoff games in rewards players whose team went deep and penalises nobody, which is backwards for a draft. It is not a rounding difference: filtering to REG moves Puka Nacua from 19 games to 16, and A.J. Brown’s average from 14.1 to 14.7.
Filter to the regular seasonpythonweeks = [w for w in resp["data"] if w["season_type"] == "REG"] - 05
Score consistency, not just the average
This is the step the other tutorials do not reach, and it is the only reason a draft board beats a leaderboard. From the weekly values compute a floor (the median of the worst quarter of weeks), a boom rate and a bust rate. Ranking by floor rather than by average is what changes picks.
Consistency from weekly PPRpythonimport statistics def consistency(weeks): pts = [w["fantasy_points_ppr"] for w in weeks] worst_quarter = sorted(pts)[: max(1, len(pts) // 4)] return { "games": len(pts), "ppr_pg": round(statistics.mean(pts), 1), "floor": round(statistics.median(worst_quarter), 1), "boom": round(100 * sum(p >= 20 for p in pts) / len(pts)), "bust": round(100 * sum(p < 10 for p in pts) / len(pts)), } - 06
Check he is still the WR1 on his own team
A great 2025 means nothing if someone was signed over him in March. The depth chart is keyed by team and position and carries the 2026 season — this is the one endpoint on the page whose data changes daily, so it is the one worth calling fresh on draft morning.
GET /v1/nfl/depth-chartsbashcurl -s "https://api.bigballsdata.com/v1/nfl/depth-charts?team=MIN&position=WR&season=2026" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": [ { "team": "MIN", "player_name": "Justin Jefferson", "season": 2026, "position": { "abbreviation": "WR", "rank": 1 }, "updated_at": "2026-08-28T09:03:10.758Z" }, { "team": "MIN", "player_name": "Jordan Addison", "season": 2026, "position": { "abbreviation": "WR", "rank": 2 }, "updated_at": "2026-08-28T09:03:10.758Z" }, { "team": "MIN", "player_name": "Jauan Jennings", "season": 2026, "position": { "abbreviation": "WR", "rank": 3 }, "updated_at": "2026-08-28T09:03:10.758Z" } ], "pagination": { "total": 7 } } - 07
Add schedule strength with team Elo
Elo gives you opponent quality as one number, which a win-loss record cannot. Use it to weight a player’s outlook by the team around him. A rating near 1500 is average; see the Elo reference for how to read the spread.
GET /v1/teams/:id/elobashcurl -s "https://api.bigballsdata.com/v1/teams/5e2e55b4-ca5b-416c-909b-1fa2e7b9ed20/elo" \ -H "x-api-key: $BBS_API_KEY"Real response, trimmed
jsonjson{ "data": { "name": "Minnesota Vikings", "abbreviation": "MIN", "elo_rating": 1496.3, "elo_rank": null, "games_counted": 106, "last_computed": "2026-09-04T11:41:41.255Z", "upgrade": { "locked": ["elo_rank", "elo/history", "win_probability"], "message": "You are seeing the rating. Rank, the full rating history and model win probabilities are on Solo." } } } - 08
Run it and get the board
The whole thing, end to end. Set BBS_API_KEY, run it, and you get a ranked board. The output below is the real result for the 2025 regular season — not a mock-up.
draft_board.pypythonimport os, statistics, urllib.parse, urllib.request, json API = "https://api.bigballsdata.com" KEY = os.environ["BBS_API_KEY"] YEAR = 2025 def get(path): req = urllib.request.Request(API + path, headers={"x-api-key": KEY}) with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) def player_id(name): q = urllib.parse.quote(name) hits = get(f"/v1/players?name={q}&sport=american_football")["data"] nfl = [p for p in hits if p.get("league_name") == "NFL"] if not nfl: raise LookupError(name) return nfl[0]["id"], nfl[0]["team_name"] def board(names): rows = [] for name in names: pid, team = player_id(name) resp = get(f"/v1/players/{pid}/stats?sport=american_football&season={YEAR}") # Regular season only — see step 4. pts = [w["fantasy_points_ppr"] for w in resp["data"] if w["season_type"] == "REG" and w["fantasy_points_ppr"] is not None] if len(pts) < 8: # too thin to rank on continue worst = sorted(pts)[: max(1, len(pts) // 4)] rows.append({ "player": name, "team": team, "games": len(pts), "ppr_pg": round(statistics.mean(pts), 1), "floor": round(statistics.median(worst), 1), "boom": round(100 * sum(p >= 20 for p in pts) / len(pts)), "bust": round(100 * sum(p < 10 for p in pts) / len(pts)), }) # Floor first: that is the whole point of the board. return sorted(rows, key=lambda r: -r["floor"]) if __name__ == "__main__": print(f"{'PLAYER':<20}{'GP':>3}{'PPR/G':>7}{'FLOOR':>7}{'BOOM':>6}{'BUST':>6}") for r in board(["Justin Jefferson", "Ja'Marr Chase", "CeeDee Lamb", "Amon-Ra St. Brown", "A.J. Brown", "Puka Nacua", "Nico Collins"]): print(f"{r['player']:<20}{r['games']:>3}{r['ppr_pg']:>7}" f"{r['floor']:>7}{r['boom']:>5}%{r['bust']:>5}%")Real response, trimmed
jsonjsonPLAYER GP PPR/G FLOOR BOOM BUST Puka Nacua 16 23.4 13.8 62% 6% CeeDee Lamb 13 15.5 9.6 23% 15% Amon-Ra St. Brown 17 19.1 7.3 41% 24% Ja'Marr Chase 16 19.6 6.7 50% 25% Nico Collins 15 15.1 6.7 33% 33% Justin Jefferson 17 11.9 3.7 6% 35% A.J. Brown 15 14.7 2.7 27% 33% - 09
Read the board — this is where it pays off
Sorted by average, Ja’Marr Chase (19.6) outranks CeeDee Lamb (15.5) comfortably. Sorted by floor, Lamb (9.6) is ahead of Chase (6.7): Lamb busts in 15% of weeks, Chase in 25%. A.J. Brown is the sharpest case — a respectable 14.7 average sitting on a 2.7 floor and a third of weeks under ten points. Those are the picks a season-average leaderboard cannot warn you about, and the whole difference is having every week rather than one number.
Reference
Every endpoint this tutorial uses
All on the gateway at api.bigballsdata.com. Verified against production on 2026-09-03.
| Method | Path | Returns | Why you need it | Plan |
|---|---|---|---|---|
| GET | /v1/coverage?sport=american_football | League keys, match counts and the seasons held for each | Confirms your key works and tells you which seasons exist before you write a query against one that does not. | Free |
| GET | /v1/players?name=&sport=american_football | Player id, position, team id and team name | The name-to-id step. Also returns team_id, which is what the Elo call needs — so you do not need a separate team lookup. | Free |
| GET | /v1/players/:id/stats?sport=american_football | Per-week receiving, rushing and passing lines with fantasy_points_ppr | The spine of the board. PPR points are precomputed per week, so consistency is a mean and a median away rather than a scoring engine away. The endpoint is free; the free tier is scoped to the last completed season, and the 2020–2025 archive is Solo. | Free |
| GET | /v1/nfl/depth-charts | Team, player, position rank and the validity window for that placement | Answers whether last season’s production still belongs to the same role. The only endpoint here whose rows change daily. | Free |
| GET | /v1/teams/:id/elo | elo_rating, games_counted, last_computed (rank withheld on free) | Schedule strength as one number — a rating near 1500 is average, and the NFL pool runs roughly 1338 to 1713. How Elo works, and how to read a rating →Recomputed by a batch replay rather than after each game — read last_computed rather than assuming it reflects last night. | Free |
| GET | /v1/nfl/games?season=2026 | The full 272-game 2026 regular-season schedule with dates and venues | Turns a static ranking into a weekly plan — which opponent, and when. | Free |
Pricing, honestly
Where the free tier stops
Everything above runs on a free key against the 2025 season. Two things are the paid wedge, and both are visible in the responses rather than hidden behind a 403: the multi-season archive, and the ranked half of Elo. The free response tells you what is withheld and why instead of silently returning less.
Free key
{
"elo_rating": 1496.3,
"elo_rank": null,
"upgrade": {
"locked": ["elo_rank", "elo/history", "win_probability"],
"message": "You are seeing the rating. Rank, the full rating history and model win probabilities are on Solo.",
"url": "https://bigballsdata.com/pricing"
}
}Solo
{
"elo_rating": 1496.3,
"elo_rank": 12,
"games_counted": 106,
"last_computed": "2026-09-04T11:41:41.255Z"
}- Free covers the last completed season — 2025 — which is every call in this tutorial.
- Solo unlocks the full 2020–2025 weekly archive: 112,333 player-weeks, with fantasy_points_ppr populated on every one.
- Solo also unlocks elo_rank, the rating history and model win probabilities. The rating itself stays free.
- Six seasons is what makes a projection rather than a ranking — one season tells you who was good, several tell you who repeats.
More tutorials
Other build guides
Known gaps
What is not here yet
Three things a fantasy build would reasonably expect, which this API does not honestly provide today. They are listed here rather than discovered at step 6, and none of them is coming-soon copy for something that already works.
- NFL injuries are not usable. The endpoint answers 200, which is the problem: on 2026-09-03 it returned four rows across two of thirty-two teams, all a week old, and the table holds nothing at all for the 2026 season. Basketball and hockey injuries are genuinely live — American football is not, and a draft board that showed four injuries would be worse than one that shows none.
- There is no in-season 2026 scoring yet, because the season opens on 2026-09-09. Weekly stats begin landing after week 1; until then every rate on this page is a 2025 number and should be read as one.
- NFL standings report preseason results. Six days before kickoff the 2026 table shows teams at 3-0 from exhibition games, so this tutorial does not use it. Use the schedule endpoint for fixtures and Elo for strength.
- No projections, no ADP and no ownership. This is measured history plus a rating; if you want somebody else’s forecast, this is not that API.
Questions
- Can I build a fantasy football app on this API?
- Yes, and the draft board above is a working piece of one. What you get is the data layer: per-week PPR scoring back to 2020, the current depth chart, team Elo and the full 2026 schedule. What you do not get is league management — rosters, matchups, waivers and scoring settings are your application’s, not ours. If you want the fantasy platform itself rather than the data underneath it, this is the wrong layer.
- Do I have to compute PPR points myself?
- No. fantasy_points_ppr arrives precomputed on every weekly row and is populated on all 112,333 of them across the six seasons held. If your league uses half-PPR or a custom scoring rule, the component stats — receptions, targets, yards, touchdowns — are on the same row, so you can rescore from them without a second call.
- Why does the tutorial use 2025 rather than the current season?
- Because the 2026 season had not kicked off when this was written — the opener is 2026-09-09 and no regular-season game had been played. 2025 is the most recent complete season and is also what a free key is scoped to. Once week 1 lands, changing the YEAR constant is the only edit needed.
- Why is there no injury step?
- Because NFL injury data is not currently usable, and a step built on it would be dishonest. On 2026-09-03 the endpoint returned four rows covering two of thirty-two teams, all stamped a week earlier, with nothing at all for the 2026 season. It answers 200, which makes the gap easy to miss. Injuries for basketball and ice hockey are live and do support this kind of step.
- Do I need the MCP server, or can I just call the REST API?
- Either. For this build the agentic path is close to complete — find_players and get_player_stats cover the draft board end to end, which is unusual; most of our tutorials need a REST fallback for their best call. The one gap is the depth chart, which has no MCP tool, so an agent should fetch GET /v1/nfl/depth-charts over plain HTTP with the same key rather than inferring roles from usage.
Related APIs
Other APIs you can build with: