Skip to navigation

Sample responses

Real responses from a recorded NFL game and a Premier League match. No key or account needed, and the NFL game replays play by play.

Everything on this page is what the API returned for two real games, untrimmed. Read the files to see the shapes before you sign up, test your parser against them, or replay the NFL game’s journal through your client to see a whole game arrive change by change. None of it needs a key, and none of it starts your trial.

The games

GameWhat’s recorded
NFLBills 23 at Vikings 6, Monday Night Football, November 9, 2026The whole game, polled every 3 seconds from before kickoff to after the final: 187 plays, 25 drives, 68 players and 1,277 changes, plus a snapshot from the 4th quarter
Premier LeagueManchester United 4, Aston Villa 4, November 8, 2026The finished match: 106 events, both squads and every player’s stats

The files were made by replaying the recorded upstream responses through Statpad’s real ingest pipeline into a fresh database and requesting each endpoint from the real API, so they’re the exact bodies you’d get. Two things differ from production: the ids are that database’s, and every updated_at and change at is when the sample was generated, not when the game was played.

Files

FileSame asSize
game-live.jsonGET /v1/nfl/games/{id} with 7:02 left in the 4th quarter2 KB
game.jsonGET /v1/nfl/games/{id} after the final2 KB
plays.jsonGET /v1/nfl/games/{id}/plays, all 187 plays230 KB
play-groups.jsonGET /v1/nfl/games/{id}/play-groups, all 25 drives16 KB
boxscore.jsonGET /v1/nfl/games/{id}/boxscore34 KB
roster.jsonGET /v1/nfl/games/{id}/roster, with the players55 KB
changes.ndjsonEvery change on game:{id}, one per line: what GET /v1/nfl/games/{id}/changes pages through and the stream delivers839 KB

The files allow requests from any origin, so you can fetch them from a browser too:

curl -s https://statpad.dev/samples/nfl-bills-at-vikings/game-live.json | jq .live_state

A game in progress

game-live.json is the game with 7:02 left in the 4th quarter, the Bills up 13 to 6 with 1st and goal at the Minnesota 5. This is its live_state, unedited:

{
"away_score": 13,
"basketball": null,
"clock_running": null,
"clock_seconds": 422,
"football": {
"away_timeouts": 3,
"down": 1,
"home_timeouts": 2,
"yardline": 95,
"yards_to_go": 5
},
"game_id": "01a1d60f-67be-7bdd-83f4-53286521d9bb",
"home_score": 6,
"last_play_id": "01a1d60f-8d90-7a74-8905-46b0dc0d2751",
"period": 4,
"period_half": null,
"possession_team_id": "01a1d60f-67be-7bdf-ac2f-bcdeeac1da68",
"soccer": null,
"updated_at": "2026-11-13T17:58:25.937181Z",
"version": 264
}

Its seq is 1101. Line 1,102 of changes.ndjson is the first change after it, which is where the Quickstart client would resume. Four changes later, at seq 1106, James Cook runs it in.

Replay the game

changes.ndjson is the game’s whole journal, in seq order, exactly as the stream delivers it to a client that resumes from seq 0. Feeding it through your code is the closest thing to watching the game live, any day of the week.

This script plays it back and prints each play as it lands. It keeps the same map as the Quickstart client, keyed by entity type and id, with the newest version winning, so it also shows corrections arriving:

replay.mts
const BASE = "https://statpad.dev/samples/nfl-bills-at-vikings";
const rate = Number(process.argv[2] ?? 20);
const detail = await (await fetch(`${BASE}/game.json`)).json();
const alias = new Map([detail.home_team, detail.away_team].map((t) => [t.id, t.alias]));
const home = alias.get(detail.game.home_team_id);
const away = alias.get(detail.game.away_team_id);
const lines = (await (await fetch(`${BASE}/changes.ndjson`)).text()).trim().split("\n");
// The same map the quickstart keeps: entity_type:entity_id -> the newest version seen.
const state = new Map<string, { version: number; payload: any }>();
const clock = (s: number | null) => (s == null ? "" : `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`);
for (const line of lines) {
const c = JSON.parse(line);
await new Promise((resolve) => setTimeout(resolve, 1000 / rate));
const key = `${c.entity_type}:${c.entity_id}`;
const held = state.get(key);
if (held && held.version >= c.version) continue;
state.set(key, { version: c.version, payload: c.op === "delete" ? null : c.payload });
if (c.entity_type !== "play" || c.op !== "upsert") continue;
const p = c.payload;
const score = `${away} ${p.away_score ?? "-"}, ${home} ${p.home_score ?? "-"}`;
const note = held ? " (corrected)" : "";
console.log(`#${c.seq} Q${p.period} ${clock(p.clock_seconds).padStart(5)} ${score} ${p.description}${note}`);
}

It needs no packages. Run it with Node 22.18 or newer; the argument is changes per second, so the default of 20 plays the game in about a minute:

node replay.mts
#10 Q1 15:00 BUF 0, MIN 0 W.Reichard kicks 59 yards from MIN 35 to BUF 6. G.Dortch to MIN 36 for 58 yards (I.Pace).
#18 Q1 14:52 BUF 0, MIN 0 J.Cook right guard to MIN 35 for 1 yard (D.Turner).
#23 Q1 14:52 BUF 0, MIN 0 J.Cook right guard to MIN 35 for 1 yard (D.Turner; E.Wilson). (corrected)
#25 Q1 14:13 BUF 0, MIN 0 PENALTY on BUF-S.Brown, False Start, 5 yards, enforced at MIN 35 - No Play.
...
#1106 Q4 6:17 BUF 19, MIN 6 J.Cook up the middle for 5 yards, TOUCHDOWN.
#1111 Q4 6:17 BUF 20, MIN 6 J.Cook up the middle for 5 yards, TOUCHDOWN. T.Bass extra point is GOOD, Center-R.Ferguson, Holder-T.Doman. (corrected)
...
#1275 Q4 0:00 BUF 23, MIN 6 END GAME
#1277 Q1 2:19 BUF 0, MIN 0 J.Cook left end to MIN 21 for -2 yards (E.Wilson).Penalty on BUF-T.Grable, Offensive Holding, declined. (corrected)

Corrections are normal. In this game, 83 of the 270 play changes rewrite a play that already arrived: a second tackler added, a touchdown that gains its extra point, a first-quarter penalty noted after the final whistle. Each is an upsert with a higher version, so replacing by id keeps you right without special handling.

To run your real client against the file, wrap each line in the message the stream sends, { "type": "changes", "stream": "game:<id>", "changes": [change] }, and pass it to the handler you use for the WebSocket.

Replay a recent game on the API

With a key, a game replays the same way on the real API, from the first change, for at least 7 days after it ends:

  • Over the stream, subscribe with resume set to 0: { "type": "subscribe", "streams": ["game:<id>"], "resume": { "game:<id>": 0 } }. The server sends every change from the start, then carries on live if the game is still going. Replay doesn’t use your request allowance.
  • Over REST, page GET /v1/{league}/games/{id}/changes?after_seq=0&limit=1000, passing the last seq of each page as the next after_seq. Each page counts as one request against your allowance.

From a position older than what’s retained, the stream sends resync_required and /changes answers 410. The game’s plays, drives and box score are still there over REST.

What to expect in the data

These samples show the API as it is today, including the fields nothing fills yet. A few you’ll notice:

  • period_scores and the box score’s teams are empty lists. Add up the player lines for team totals.
  • In the NFL game, clock_running is null: the football feed has no running flag.
  • After the final, live_state.football keeps the last down, distance and yard line, and its timeouts are null, because the feed stops reporting them.
  • NFL plays have an empty participants list; the description names the players.
  • Premier League events have no group_id or clock_seconds. Their time is soccer.minute and soccer.added_minute.

Coverage by league lists every field and endpoint that isn’t filled yet, per league.

The Premier League match is a finished-match sample, so it has no journal to replay. A recorded Premier League match will join the NFL game here.