Skip to content
The Margin
Open data

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_version
integer
Version of this file's column contract. Columns are never renamed or removed within a version.
as_of
datetime, ISO 8601, UTC
When the numbers in this file were computed (the source payload's generation time).
season
integer
NFL season (the year it starts).
through_week
integer
Last regular-season week whose games are counted.
team
string, nflverse team code
Team, e.g. KC, SF, LA.
off_plays
integer, may be empty
Offensive plays counted: passes and rushes that carry an EPA value.
def_plays
integer, may be empty
Opponent plays counted, same rule.
off_epa_play
number, EPA per play, may be empty
Offense: expected points added per play, season to date. Higher is better.
def_epa_play
number, EPA per play, may be empty
Defense: EPA per play allowed, season to date. Lower is better.
off_epa_play_last3
number, EPA per play, may be empty
Offense EPA per play over the team's last 3 games (byes skipped).
def_epa_play_last3
number, EPA per play, may be empty
Defense EPA per play allowed over the team's last 3 games.
off_epa_play_last1
number, EPA per play, may be empty
Offense EPA per play in the team's most recent game.
def_epa_play_last1
number, EPA per play, may be empty
Defense EPA per play allowed in the team's most recent game.
off_pass_epa_play
number, EPA per play, may be empty
Offense EPA per pass play (dropbacks, including sacks and scrambles).
def_pass_epa_play
number, EPA per play, may be empty
Defense EPA per pass play allowed.
off_rush_epa_play
number, EPA per play, may be empty
Offense EPA per designed run.
def_rush_epa_play
number, EPA per play, may be empty
Defense EPA per designed run allowed.
off_success_rate
number, share, 0-1, may be empty
Share of offensive plays with EPA above zero.
def_success_rate
number, 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_version
integer
Version of this file's column contract. Columns are never renamed or removed within a version.
as_of
datetime, ISO 8601, UTC
When the numbers in this file were computed (the source payload's generation time).
season
integer
NFL season.
through_week
integer
Last week whose games are counted.
player_id
string, nflverse gsis_id
Stable player id, e.g. 00-0033873. Joins to nflverse data.
name
string
Short name as nflverse spells it, e.g. P.Mahomes.
team
string, nflverse team code
Team in his most recent game.
games
integer
Games played this season.
plays
integer
Plays graded: dropbacks, his designed runs, and pass penalties.
dropbacks
integer
Dropbacks (passes, sacks, scrambles, spikes).
grade
number, 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_lo80
number, 0-100, 50 = average, may be empty
Low end of a rough 80% range for grade.
grade_hi80
number, 0-100, 50 = average, may be empty
High end of that range.
right_now
number, 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.
career
number, 0-100, 50 = average, may be empty
The grade over every play of his career, shrunk by 100 plays.
letter
string, 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_version
integer
Version of this file's column contract. Columns are never renamed or removed within a version.
as_of
datetime, ISO 8601, UTC
When the numbers in this file were computed (the source payload's generation time).
season
integer
NFL season.
week
integer
Regular-season week.
game_id
string, {season}_{week}_{away}_{home}
nflverse game id, e.g. 2026_05_TB_DAL. Same id as the frozen weekly CSV.
kickoff_utc
datetime, ISO 8601, UTC, may be empty
Scheduled kickoff.
away
string, nflverse team code
Away team.
home
string, nflverse team code
Home team.
neutral
boolean, true | false
Neutral site: no home-field advantage applied.
model_margin_home
number, points
Our projected home margin. Positive = home team favored.
model_home_win_prob
number, probability, 0-1
Our probability that the home team wins.
market_spread_home
number, 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_total
number, points, may be empty
The book's total points line.
market_home_ml
integer, American odds, may be empty
The book's home moneyline.
market_away_ml
integer, American odds, may be empty
The book's away moneyline.
market_book
string, may be empty
Sportsbook the line came from, lower case (e.g. draftkings).
market_as_of
datetime, 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_version
integer
Version of this file's column contract. Columns are never renamed or removed within a version.
as_of
datetime, ISO 8601, UTC
When the numbers in this file were computed (the source payload's generation time).
season
integer
College football season.
week
integer
CFBD week number within season_type.
season_type
string, regular | postseason
Regular season or postseason.
game_id
string, 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_utc
datetime, ISO 8601, UTC, may be empty
Scheduled kickoff.
away
string, CFBD school name
Away team.
home
string, CFBD school name
Home team.
neutral
boolean, true | false
Neutral site: no home-field advantage applied.
completed
boolean, true | false
The game has been played.
model_margin_home
number, points
Our projected home margin. Positive = home team favored.
model_total
number, points, may be empty
Our projected total points.
model_home_win_prob
number, probability, 0-1
Our probability that the home team wins.
market_spread_home
number, 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_total
number, points, may be empty
The book's total points line.
market_home_ml
integer, American odds, may be empty
The book's home moneyline.
market_away_ml
integer, American odds, may be empty
The book's away moneyline.
market_book
string, may be empty
Sportsbook the line came from, lower case (e.g. draftkings).
market_as_of
datetime, 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_version
integer
Version of this file's column contract. Columns are never renamed or removed within a version.
as_of
datetime, ISO 8601, UTC
When the numbers in this file were computed (the source payload's generation time).
league
string, nfl | cfb
League.
season
integer
Season.
week
integer
Week the call was published for.
rule_version
string, YYYY-MM-DD, may be empty
Selection rule the sheet was published under. Empty for NFL weeks published before sheets carried one.
published_at
datetime, ISO 8601, UTC, may be empty
When that week's sheet was published. Calls are never edited after publication.
game_id
string
Game id: nflverse id for NFL, CFBD id for CFB (joins to the projections files).
kickoff_utc
datetime, ISO 8601, UTC, may be empty
Scheduled kickoff.
away
string
Away team (NFL code or CFBD school name).
home
string
Home team.
market
string, spread | moneyline | total
What was called.
slot
string, 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.
call
string
The call as published, e.g. "HOU -3.0", "DET ML -305", "UNDER 44.5 (NE@SEA)".
pick
string
Team picked, or over / under for a total.
line
number, points, may be empty
Spread for the picked team as published (-3.0 = laying 3), or the total line. Empty for a moneyline.
odds
integer, American odds, may be empty
Price as published. Empty where the sheet did not record one.
outcome
string, win | loss | push | pending | void
Graded result. pending = not yet played or not yet graded.
profit_units
number, 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_record
boolean, 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.csv

Columns

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.csv
this 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.