Recorded history is temporarily unavailable, so the download and calendar are paused. They return on their own when the data service is back. The column reference and code below work today.

Coverage calendar

Recorded change events per league per US Eastern day, with collector runs per day. Empty cells with runs are quiet boards; days with no runs are collection gaps.

The calendar loads from the recorded history and is paused while the data service is unavailable.

Columns

ColumnMeaning
change_idSequence id of the event. Same value as id and cursor in GET /api/v1/changes.
observed_atUTC time the collector saw the change (ISO 8601).
leagueLeague code as used by the API.
event_typeoffer.new, offer.changed, offer.unavailable or offer.restored.
projection_idStable projection id. Use it with GET /api/v1/projections/{id}/history.
playerPlayer name as shown on the board.
teamTeam abbreviation from the latest stored projection.
statMarket name, for example Points or Pts+Rebs+Asts.
periodMarket period, for example Full game.
more_tierTier of the More side on the latest stored projection: standard, goblin or demon.
previous_lineLine before this event. Empty for offer.new.
lineLine after this event.
line_deltaline minus previous_line when both are numbers, else empty.

RFC 4180 CSV, UTF-8, CRLF line endings. Text that a spreadsheet would run as a formula is prefixed with a single quote. Response headers carry X-Sample-Date, X-Row-Count and X-Truncated.

Load it

Python · pandas
import pandas as pd

url = "https://prizepicksapi.com/data/history-sample/sample.csv?league=NBA"
df = pd.read_csv(url, parse_dates=["observed_at"])

moves = df[(df.event_type == "offer.changed") & df.line_delta.notna() & (df.line_delta != 0)]
print(moves.groupby("stat").line_delta.describe())

# Biggest movers of the day
print(moves.reindex(moves.line_delta.abs().sort_values(ascending=False).index)
      .head(10)[["observed_at", "player", "stat", "previous_line", "line"]])
Node.js · the same day from the API
// Same events, any day since recording began. Node 18+.
const KEY = process.env.PRIZEPICKS_API_KEY;
let cursor = "0";
for (;;) {
  const url = new URL("https://prizepicksapi.com/api/v1/changes");
  url.search = new URLSearchParams({ league: "NBA", after: cursor, limit: "200" });
  const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const { data, meta } = await res.json();
  for (const e of data) console.log(e.observed_at, e.type, e.player_name, e.previous_line, "->", e.line);
  if (!meta.next_cursor) break;
  cursor = meta.next_cursor;
  await new Promise((r) => setTimeout(r, 6100)); // stay under the Free per-minute limit
}

Questions

How far back does recorded PrizePicks history go?

Recording started on 27 October 2026. History exists from the first time this service observed an offer; nobody can backfill lines that were not recorded at the time. The calendar on this page shows every recorded day by league.

What is in the CSV sample?

Every change event recorded for one league on the most recent completed US Eastern day: new offers, line moves, offers taken down and offers restored, each with its UTC timestamp, previous and new line. It is the same data as GET /api/v1/changes, flattened to one row per event.

Can I download other days or leagues?

The free sample covers the latest completed day per league. Any day since recording began is available through GET /api/v1/changes and GET /api/v1/projections/{id}/history on every plan, including Free.

Does a day with no events mean data is missing?

Not always. The calendar shows collector runs per day next to each league. Runs with no events mean the league had no board that day; no runs means the collector was down and that day is a real gap.

Keep going

Get free API key

References