Open data
Stable files for NFL team EPA, QB Grade, game projections and McConnell's Calls; frozen weekly CSVs; and the JSON behind every page.
Stable files (v1)
Five files you can script against, each as CSV and as JSON. The URLs and columns stay fixed: a column is never renamed, retyped or removed while schema_version is 1. New columns, if any, are added at the end. A change that cannot keep that promise goes to a new /v2/ path, and the /v1/ files keep working.
Free to use with credit to The Margin, themarginsports.com.
https://themarginsports.com/v1/nfl/team-epa.csv https://themarginsports.com/v1/nfl/team-epa.json https://themarginsports.com/v1/nfl/qb-grade.csv https://themarginsports.com/v1/nfl/qb-grade.json https://themarginsports.com/v1/nfl/projections.csv https://themarginsports.com/v1/nfl/projections.json https://themarginsports.com/v1/cfb/projections.csv https://themarginsports.com/v1/cfb/projections.json https://themarginsports.com/v1/calls.csv https://themarginsports.com/v1/calls.json catalog of all files, with columns and as_of: https://themarginsports.com/v1/index.json
Every row carries schema_version and as_of (when the numbers were computed, UTC). The JSON files wrap the same rows with the column definitions, the license, as_of and built_at (when the file was written). An empty CSV cell is a JSON null. Booleans are true or false; times are ISO 8601 in UTC.
The files are rewritten when the data they come from is published, in the same step that updates the pages. Check as_of rather than assuming a time. Any website can fetch them from a browser (they send Access-Control-Allow-Origin: *).
# Python
import pandas as pd
epa = pd.read_csv("https://themarginsports.com/v1/nfl/team-epa.csv")
# R
epa <- read.csv("https://themarginsports.com/v1/nfl/team-epa.csv")
// JavaScript
const { rows, as_of } = await (await fetch(
"https://themarginsports.com/v1/nfl/team-epa.json")).json();Rate limits: none of our own. The files are static and served from Cloudflare's cache, so a request every few minutes per file is plenty; they do not change more often than that. Cloudflare may block traffic that looks like an attack.
NFL team EPA per play
/v1/nfl/team-epa.csv and .json. Rows: one row per team, current season. Source updated: Weekly, Tuesday morning after the week's games (US Mountain time).
schema_versioninteger- Version of this file's column contract. Columns are never renamed or removed within a version.
as_ofdatetime, ISO 8601, UTC- When the numbers in this file were computed (the source payload's generation time).
seasoninteger- NFL season (the year it starts).
through_weekinteger- Last regular-season week whose games are counted.
teamstring, nflverse team code- Team, e.g. KC, SF, LA.
off_playsinteger, may be empty- Offensive plays counted: passes and rushes that carry an EPA value.
def_playsinteger, may be empty- Opponent plays counted, same rule.
off_epa_playnumber, EPA per play, may be empty- Offense: expected points added per play, season to date. Higher is better.
def_epa_playnumber, EPA per play, may be empty- Defense: EPA per play allowed, season to date. Lower is better.
off_epa_play_last3number, EPA per play, may be empty- Offense EPA per play over the team's last 3 games (byes skipped).
def_epa_play_last3number, EPA per play, may be empty- Defense EPA per play allowed over the team's last 3 games.
off_epa_play_last1number, EPA per play, may be empty- Offense EPA per play in the team's most recent game.
def_epa_play_last1number, EPA per play, may be empty- Defense EPA per play allowed in the team's most recent game.
off_pass_epa_playnumber, EPA per play, may be empty- Offense EPA per pass play (dropbacks, including sacks and scrambles).
def_pass_epa_playnumber, EPA per play, may be empty- Defense EPA per pass play allowed.
off_rush_epa_playnumber, EPA per play, may be empty- Offense EPA per designed run.
def_rush_epa_playnumber, EPA per play, may be empty- Defense EPA per designed run allowed.
off_success_ratenumber, share, 0-1, may be empty- Share of offensive plays with EPA above zero.
def_success_ratenumber, share, 0-1, may be empty- Share of opponent plays with EPA above zero. Lower is better.
NFL Margin QB Grade
/v1/nfl/qb-grade.csv and .json. Rows: one row per quarterback with a graded play this season. Source updated: Daily, and after each NFL game window.
schema_versioninteger- Version of this file's column contract. Columns are never renamed or removed within a version.
as_ofdatetime, ISO 8601, UTC- When the numbers in this file were computed (the source payload's generation time).
seasoninteger- NFL season.
through_weekinteger- Last week whose games are counted.
player_idstring, nflverse gsis_id- Stable player id, e.g. 00-0033873. Joins to nflverse data.
namestring- Short name as nflverse spells it, e.g. P.Mahomes.
teamstring, nflverse team code- Team in his most recent game.
gamesinteger- Games played this season.
playsinteger- Plays graded: dropbacks, his designed runs, and pass penalties.
dropbacksinteger- Dropbacks (passes, sacks, scrambles, spikes).
gradenumber, 0-100, 50 = average- Margin QB Grade for the season: points added per play on a 0-100 scale where 50 is an average QB play (2006-21), shrunk toward 50 by 100 plays. An interception counts a flat -4; no play counts more than +/-4.
grade_lo80number, 0-100, 50 = average, may be empty- Low end of a rough 80% range for grade.
grade_hi80number, 0-100, 50 = average, may be empty- High end of that range.
right_nownumber, 0-100, 50 = average, may be empty- How good he is now: career credit with past seasons down-weighted (0.45 per season back), adjusted for opposing pass defense, shrunk toward a draft-slot prior.
careernumber, 0-100, 50 = average, may be empty- The grade over every play of his career, shrunk by 100 plays.
letterstring, A | B | C | D | F, may be empty- Letter band for grade: A 75+, B 60-74, C 40-59, D 25-39, F under 25.
NFL game projections
/v1/nfl/projections.csv and .json. Rows: one row per game, from the current week to the end of the regular season. Source updated: Daily; lines are captured once a day.
Our margin and win probability are the model's own numbers, not blended with the line. We do not publish an NFL model total, so this file has the book's total only. Past weeks are not in this file: the graded pre-game numbers are the weekly frozen CSVs below.
schema_versioninteger- Version of this file's column contract. Columns are never renamed or removed within a version.
as_ofdatetime, ISO 8601, UTC- When the numbers in this file were computed (the source payload's generation time).
seasoninteger- NFL season.
weekinteger- Regular-season week.
game_idstring, {season}_{week}_{away}_{home}- nflverse game id, e.g. 2026_05_TB_DAL. Same id as the frozen weekly CSV.
kickoff_utcdatetime, ISO 8601, UTC, may be empty- Scheduled kickoff.
awaystring, nflverse team code- Away team.
homestring, nflverse team code- Home team.
neutralboolean, true | false- Neutral site: no home-field advantage applied.
model_margin_homenumber, points- Our projected home margin. Positive = home team favored.
model_home_win_probnumber, probability, 0-1- Our probability that the home team wins.
market_spread_homenumber, points, may be empty- The book's line as a home margin, SAME sign as model_margin_home: +3.5 means the book has the home team favored by 3.5. Empty when no line is posted yet.
market_totalnumber, points, may be empty- The book's total points line.
market_home_mlinteger, American odds, may be empty- The book's home moneyline.
market_away_mlinteger, American odds, may be empty- The book's away moneyline.
market_bookstring, may be empty- Sportsbook the line came from, lower case (e.g. draftkings).
market_as_ofdatetime, ISO 8601, UTC, may be empty- When the line was captured. CFB lines carry a capture date only, written as 00:00:00Z on that date.
College football game projections
/v1/cfb/projections.csv and .json. Rows: one row per FBS game we rate, from the current week to the end of the schedule. Source updated: Daily; lines are captured once a day.
Same columns as the NFL file plus season_type, completed and our projected total. Past weeks are in the weekly frozen CSVs below.
schema_versioninteger- Version of this file's column contract. Columns are never renamed or removed within a version.
as_ofdatetime, ISO 8601, UTC- When the numbers in this file were computed (the source payload's generation time).
seasoninteger- College football season.
weekinteger- CFBD week number within season_type.
season_typestring, regular | postseason- Regular season or postseason.
game_idstring, CFBD / ESPN game id- Game id as CFBD publishes it, e.g. 401866408. Same id as the frozen weekly CSV and the calls file.
kickoff_utcdatetime, ISO 8601, UTC, may be empty- Scheduled kickoff.
awaystring, CFBD school name- Away team.
homestring, CFBD school name- Home team.
neutralboolean, true | false- Neutral site: no home-field advantage applied.
completedboolean, true | false- The game has been played.
model_margin_homenumber, points- Our projected home margin. Positive = home team favored.
model_totalnumber, points, may be empty- Our projected total points.
model_home_win_probnumber, probability, 0-1- Our probability that the home team wins.
market_spread_homenumber, points, may be empty- The book's line as a home margin, SAME sign as model_margin_home: +3.5 means the book has the home team favored by 3.5. Empty when no line is posted yet.
market_totalnumber, points, may be empty- The book's total points line.
market_home_mlinteger, American odds, may be empty- The book's home moneyline.
market_away_mlinteger, American odds, may be empty- The book's away moneyline.
market_bookstring, may be empty- Sportsbook the line came from, lower case (e.g. draftkings).
market_as_ofdatetime, ISO 8601, UTC, may be empty- When the line was captured. CFB lines carry a capture date only, written as 00:00:00Z on that date.
McConnell's Calls, graded ledger
/v1/calls.csv and .json. Rows: one row per published call, NFL and CFB, every week of the season. Source updated: When a sheet is published (weekly) and as games are graded (hourly on game days).
Every call as published, never edited, with its grade. Totals of the rows where in_record is true match the record on /calls.
schema_versioninteger- Version of this file's column contract. Columns are never renamed or removed within a version.
as_ofdatetime, ISO 8601, UTC- When the numbers in this file were computed (the source payload's generation time).
leaguestring, nfl | cfb- League.
seasoninteger- Season.
weekinteger- Week the call was published for.
rule_versionstring, YYYY-MM-DD, may be empty- Selection rule the sheet was published under. Empty for NFL weeks published before sheets carried one.
published_atdatetime, ISO 8601, UTC, may be empty- When that week's sheet was published. Calls are never edited after publication.
game_idstring- Game id: nflverse id for NFL, CFBD id for CFB (joins to the projections files).
kickoff_utcdatetime, ISO 8601, UTC, may be empty- Scheduled kickoff.
awaystring- Away team (NFL code or CFBD school name).
homestring- Home team.
marketstring, spread | moneyline | total- What was called.
slotstring, may be empty- Which slot on the sheet the call filled (e.g. gap, lean, agree, lock, key_number_test). Slot names change with rule_version.
callstring- The call as published, e.g. "HOU -3.0", "DET ML -305", "UNDER 44.5 (NE@SEA)".
pickstring- Team picked, or over / under for a total.
linenumber, points, may be empty- Spread for the picked team as published (-3.0 = laying 3), or the total line. Empty for a moneyline.
oddsinteger, American odds, may be empty- Price as published. Empty where the sheet did not record one.
outcomestring, win | loss | push | pending | void- Graded result. pending = not yet played or not yet graded.
profit_unitsnumber, units per 1-unit stake, may be empty- Profit on a flat one-unit stake at the published price: a win at -110 is +0.9091, a loss -1, a push 0. Empty while pending.
in_recordboolean, true | false- Counted in the record /calls shows. CFB weeks under rules before 2026-09-28 are kept here but not counted.
Weekly frozen CSV
One row per FBS-vs-FBS game we rate, published on the Wednesday weekly cycle and frozen at publication:
https://data.margin.mcconnelldigital.com/cfb-projections-{season}-w{NN}.csv
example: .../cfb-projections-2026-w01.csvColumns
season 2026 week regular-season week number date game date (ET game date) kickoff_utc ISO kickoff time, UTC away, home school names (CFBD canonical spelling) neutral 1 = neutral site (no home-field advantage applied) margin_home projected home margin; positive = home favored total projected total points home_win_prob home win probability, 0-1 frozen_at_utc when this file was frozen
Do betting lines feed the projections?
No. The projection for a game is computed from our team ratings (a three-year SP+ blend with a talent and recruiting adjustment) plus a fixed home-field constant. No opening line, closing line, or market price is an input to that calculation, and in-season rating updates come from game results, not from line movement.
Two exceptions. First, two of the 138 teams (Sacramento State and North Dakota State) are new to FBS in 2026 and have no FBS history to rate, so their preseason priors were set from early market spreads; every projection involving those two carries that origin. Second, when we change the model we check the new version against the market as a pass or fail test before shipping it. We have rejected changes on that test, but we never fit parameters toward lines, and the model runs at its own scale even where a market-fitted scale would score better.
NFL: weekly frozen CSV
Same freeze contract as the CFB file above, one row per NFL game, published before kickoff and never rewritten:
https://data.margin.mcconnelldigital.com/nfl-projections-{season}-w{NN}.csv
example: .../nfl-projections-2026-w01.csvthis week (week 8): .../nfl-projections-2026-w08.csv [checking…]
season, week, game_id game identifier ({season}_{week}_{away}_{home})
away, home team codes
model_margin_home model's projected home margin; positive = home favored
model_home_win_prob model home win probability, 0-1
market_spread_home book's home margin, SAME sign convention as
model_margin_home (positive = home favored)
market_total book's total
market_home_ml book's home moneyline (American odds); empty
from week 2: the freeze stores no moneyline
market_away_ml book's away moneyline (American odds); same
edge_spread_pts model_margin_home - market_spread_home
frozen_at_utc when these numbers were frozen (the weekly freeze,
from week 2 on)One file per week ships as the season plays out, built from the model's own numbers and the line as frozen each Thursday, before that week's first kickoff; a superseded file is preserved alongside any correction, same as CFB. The files for weeks 2 and 3 were posted after those games, from the numbers frozen before kickoff. The week number above is derived from the calendar (weeks roll over on Tuesday), so the line names the current week's file even before it has posted.
JSON behind every page
Every page on The Margin reads static JSON from this same R2 bucket at build/runtime. There is no private API. The mapping from an endpoint path to its public file is mechanical: take/api/<path>?<query>, replace each / in the path with __, append __ plus the query string sorted by key, replace any character outside [A-Za-z0-9_=&.-] with _ (case is preserved), and add .json. A path with no query is just <path>.json.
/api/cfb-games?season=2026 → https://data.margin.mcconnelldigital.com/cfb-games__season=2026.json /api/algoedge/nfl/upcoming?season=2026&week=1 → https://data.margin.mcconnelldigital.com/algoedge__nfl__upcoming__season=2026&week=1.json /api/algoedge/nfl/v6/predictions/track-record?by=team&source=canonical → https://data.margin.mcconnelldigital.com/algoedge__nfl__v6__predictions__track-record__by=team&source=canonical.json /api/cbb-ratings → https://data.margin.mcconnelldigital.com/cbb-ratings.json
Fetch any of these directly: no auth, no key, no rate limit we enforce (be reasonable; it's a CDN, not a database). Cached ~5 minutes at the edge, refreshed on our publish cycle. Fair use: take what you need for a tracker, a grader, a research notebook. Free to use with credit to The Margin, themarginsports.com.
Which files count
The frozen CSV is the graded record. The JSON behind our site pages recomputes daily as inputs move and is not an archive; grade us on the frozen files only. If a frozen file ever has to be corrected, the prior version is preserved alongside it and the change is noted, never silently rewritten. Projections come from the same model shown on the site, ratings frozen preseason and updated on a stated schedule in season.
More ways to follow along
RSS feed of articles and model-log entries, if you'd rather read this in a reader than check back on the site.
Questions or want a different format? Say so on X and we will make it work.