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. Changes arrive within a few minutes of ESPN's injury report, so polling every 2 to 5 minutes keeps you current; alert when a starter flips to Out.

Endpoint
/injuries/changes
Plan
Pro
Lookback
14 days
Checked
Every 2 min
Watch it run: the Python poller below, on a four-player roster with the cursor set to last Thursday (1:04, with captions).

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, every 2 minutes. 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.
  • Offensive linemen (C, G, OT) are in the feed too, since they're on Pro. Filter with ?position=OT, or drop them in code if you only alert on fantasy positions. See the offensive line guide.
  • last_checked_at is when the detector last ran, even if it found nothing. Use it to tell "quiet" from "stale".

How fast do changes arrive?

GridIron checks ESPN's league-wide injury report every 2 minutes from 8:00 am to 1:00 am ET, and every 10 minutes overnight. When ESPN updates a player, the change is in this feed (and sent to your webhook, if you registered one) within a few minutes, on any day of the week: a Wednesday practice report, a Friday IR move, or Sunday inactives. Scheduled refreshes back that up:

What updates injury status (Eastern time)
WhenWhat runsCatches
Every 2 minutes, 8:00 am–1:00 amESPN injury report checkPractice reports, designations, IR moves, inactives
Every day, 11:00 amFull refresh of every playerAnything the report check missed
Every day, 12:30 pmSleeper cross-checkPlayers ruled out on Sleeper before ESPN
Sunday 12:45, 3:50 and 7:45 pm; Thursday and Monday 7:15 pmFull refresh after inactivesFinal statuses before kickoff

Request budget: polling every 5 minutes from 8:00 am to 1:00 am ET is 204 calls a day, about 6,100 a month, or 12% of Pro's 50,000. Every 2 minutes over the same hours is about 15,300 a month. If you'd rather not poll at all, register a webhook and changes are pushed to you.

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?

Poll every few minutes through the hours news breaks. With cron (CRON_TZ keeps it on Eastern time):

CRON_TZ=America/New_York
# Every 5 minutes, 8:00 am to 12:55 am
*/5 8-23,0 * * *         python poll_injuries.py
# Every 2 minutes in the hour before Sunday's kickoffs (inactives)
*/2 11-12,14-15,18-19 * * SUN  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 2 to 5 minutes. GridIron checks ESPN's injury report every 2 minutes from 8:00 am to 1:00 am ET (every 10 minutes overnight), so polling faster only spends requests. Every 5 minutes over those hours is about 6,000 requests a month.

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. The free plan covers player lookups and this week's projections; the other endpoints start at Basic ($5/month).