Guide

Track NFL injury changes on game day with the GridIron API

Quick answer: Poll GET /api/v1/injuries/changes?since= with a Pro key, save the next_since it returns, and send it back on the next call. You get only players whose status changed. Time your polls to the refresh windows (Sunday about 12:55, 4:00 and 7:55 pm ET) and alert when a starter flips to Out.

Endpoint
/injuries/changes
Plan
Pro
Lookback
14 days
Sunday runs
3 + daily

Why poll for changes instead of statuses?

Because on a Sunday morning almost nothing changes, and the part that does is the only part you care about. Pulling every player's status and diffing it yourself means storing yesterday's snapshot, handling players who appear and disappear, and burning requests on 650 rows that didn't move.

The changes feed does that diff on our side after every refresh. Your code gets a short list (previous_status, status, detected_at) and a cursor. That's the whole contract.

How does the since cursor work?

  • since is ISO-8601 (2026-09-27T16:00:00Z) or unix seconds. It's exclusive. Leave it off and you get the last 24 hours.
  • next_since is the time to send next. Store it and you'll never see a change twice or miss one.
  • limit defaults to 500 (max 1,000). A refresh run is never split across two responses; truncated: true tells you to call again right away.
  • position and team filter server-side, e.g. ?position=WR&team=BUF.
  • last_checked_at is when the detector last ran, even if it found nothing. Use it to tell "quiet" from "stale".

When do the game-day refresh windows run?

The schedule follows the NFL's news cycle: teams announce inactives about 90 minutes before kickoff, so each Sunday refresh runs after the announcements for its slate and before kickoff.

Injury refresh schedule (Eastern time; changes are recorded about 10 minutes after each refresh)
WhenRefreshChanges recordedCatches
Every day11:00 am11:12 amPractice reports and overnight moves
Sunday12:45 pm12:55 pmInactives for the 1:00 pm games
Sunday3:50 pm4:00 pmInactives for the 4:05 and 4:25 pm games
Sunday7:45 pm7:55 pmSunday Night Football inactives
Thursday and Monday7:15 pm7:25 pmFinal statuses before the night game

The schedule is pinned to Eastern time, so it doesn't drift at the November 1 clock change. In UTC that's 16:55, 20:00 and 23:55 on Sundays until then, and an hour later after it.

Request budget: polling every 5 minutes from 12:30 to 8:30 pm ET on a Sunday is 97 calls. Add Thursday, Monday and the daily run and a whole week is under 150 requests, or about 0.3% of Pro's 50,000 a month.

How do I poll the feed in Python?

This script is safe to run from cron. It keeps its cursor in a file, alerts only on your roster, and sends to a Discord webhook if you set one (Slack incoming webhooks work the same way; change content to text).

import os, pathlib, requests

API = "https://api.gridirondata.com/api/v1"
KEY = os.environ["GRIDIRON_API_KEY"]           # a Pro key
WEBHOOK = os.environ.get("DISCORD_WEBHOOK")     # optional
CURSOR = pathlib.Path("injury_cursor.txt")
ROSTER = {"Saquon Barkley#RB", "Puka Nacua#WR", "Brock Bowers#TE"}

def is_out(status):
    s = status or ""
    return s in ("Out", "Doubtful", "INACTIVE") or s.startswith(("IR", "PUP", "RESERVE"))

def severity(prev, new):
    if is_out(new) and not is_out(prev):
        return "BENCH"      # was playable, now isn't
    if is_out(prev) and not is_out(new):
        return "BACK"       # returning: consider starting
    if new == "Questionable":
        return "WATCH"
    return "INFO"

def poll():
    params = {"since": CURSOR.read_text().strip()} if CURSOR.exists() else {}
    r = requests.get(f"{API}/injuries/changes", headers={"x-api-key": KEY},
                     params=params, timeout=30)
    if r.status_code == 403:
        raise SystemExit(r.json()["error"])        # not a Pro key
    r.raise_for_status()
    body = r.json()
    for c in body["changes"]:
        if c["player_id"] not in ROSTER:
            continue
        level = severity(c["previous_status"], c["status"])
        msg = (f"[{level}] {c['player_name']} ({c['position']}, {c['team']}): "
               f"{c['previous_status']} -> {c['status']}")
        print(msg)
        if WEBHOOK and level != "INFO":
            requests.post(WEBHOOK, json={"content": msg}, timeout=10)
    CURSOR.write_text(body["next_since"])          # advance only after alerting
    return body["truncated"]

while poll():        # drain a truncated window
    pass

Two details matter. The cursor is saved after alerts go out, so a crash mid-run replays rather than drops a change. The loop drains truncated windows; you'll only hit one if you've been offline for days.

How do I poll it in JavaScript?

The same logic for Node 18+ (built-in fetch), shaped for a Discord bot or a serverless function. Keep since somewhere durable, such as a KV store or a file.

const API = "https://api.gridirondata.com/api/v1";
const KEY = process.env.GRIDIRON_API_KEY;   // a Pro key
const ROSTER = new Set(["Saquon Barkley#RB", "Puka Nacua#WR"]);

const isOut = (s = "") =>
  ["Out", "Doubtful", "INACTIVE"].includes(s) || /^(IR|PUP|RESERVE)/.test(s);

export async function pollInjuries(since, notify) {
  const url = new URL(`${API}/injuries/changes`);
  if (since) url.searchParams.set("since", since);
  const res = await fetch(url, { headers: { "x-api-key": KEY } });
  if (res.status === 403) throw new Error((await res.json()).error);
  if (res.status === 429) return since;       // rate limited: keep the cursor, retry later
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  for (const c of body.changes) {
    if (!ROSTER.has(c.player_id)) continue;
    const flipped = isOut(c.status) !== isOut(c.previous_status);
    if (flipped || c.status === "Questionable") {
      await notify(`${c.player_name} (${c.team}): ${c.previous_status} → ${c.status}`);
    }
  }
  return body.next_since;                     // store this for the next call
}

How do I schedule the poller?

Line your polls up with the windows instead of polling all day. With cron (CRON_TZ keeps it on Eastern time):

CRON_TZ=America/New_York
# Sunday windows: 12:55, 4:00 and 7:55 pm, polled for about half an hour each
55 12 * * SUN            python poll_injuries.py
0-25/5 13 * * SUN        python poll_injuries.py
0-25/5 16 * * SUN        python poll_injuries.py
55 19 * * SUN            python poll_injuries.py
0-25/5 20 * * SUN        python poll_injuries.py
# Thursday and Monday night, and the daily run
25-50/5 19 * * MON,THU   python poll_injuries.py
15 11 * * *              python poll_injuries.py

Missing a poll is harmless. The cursor means the next one picks up everything since the last success.

Which status changes deserve an alert?

Not all of them. A league-wide Sunday feed can carry dozens of flips, and an alert channel that pings for every practice-squad tight end gets muted by the second week. What's worked for us:

  • Page on: a rostered player going from anything playable to Out, Doubtful, Inactive or a reserve list. That's a lineup change you have to make before kickoff.
  • Notify on: a player coming off Out or IR. That's the start you'd otherwise miss.
  • Log only: Healthy to Questionable on Wednesday or Thursday. Most Questionable players play; the Sunday run tells you which ones don't.

Pair the alert with the next-man-up. Our Week 3 usage report shows why: when Michael Pittman Jr. sat in Week 2, Germie Bernard went from 4% to 79% of Pittsburgh's snaps. One call to /snaps for the injured player's teammates turns "he's out" into "here's who plays".

What errors should my poller handle?

  • 403 pro_required: the key isn't Pro. The body has an upgrade_url.
  • 429: you hit a rate limit. Keep your cursor and retry on the next tick.
  • 400: a malformed since. Use ISO-8601 with a Z or offset, or unix seconds, and URL-encode a +.

Frequently asked questions

How often should I poll /injuries/changes?

Every 5 minutes during the refresh windows is plenty, since changes are recorded about 10 minutes after each refresh. Outside the windows nothing new arrives, so polling then only spends requests.

What does since mean exactly?

It is exclusive: you get changes detected strictly after that time. Send the next_since from your previous response and you never see a change twice or miss one.

What happens if I pass a since older than 14 days?

It is clamped to 14 days ago and the response includes a note saying so.

Can I filter to my own players?

The feed filters by position and team. For a roster, filter the returned changes by player_id on your side; one call covers the whole league.

Does a Free or Basic key work?

No. The feed is Pro only; other keys get HTTP 403 with reason pro_required. Every other endpoint works on the free plan.