Tutorial · 11 min · endpoints verified 2026-09-11

NFL live game center API tutorial

Build a game center that shows more than a score: current drive, win probability by quarter, and the play-level EPA that explains why the number moved. About fifteen minutes; match lookup is free and NFL play-by-play requires Pro.

API coverage and endpoint overview: Explore the NFL API

Why this is hard

The problem with live NFL data

A live score is table stakes. What makes a game center worth opening mid-game is context: which quarter, whose ball, how the win probability got to where it is — and that needs both a matches feed for the score and a play-by-play feed for the drive underneath it, kept in sync.

  • Score and play-by-play usually come from different endpoints with different keys: a match id here, a text game id there. This build uses both and shows exactly where they connect.
  • Live coverage is real but polled, not pushed: the match response says plainly when a game is being served from the stored tier rather than a live adapter, so a client can show a correct "last updated" instead of implying second-by-second push.
  • Win probability is a per-play field, not a separate model call — it is already computed on every row of play-by-play, so a chart is a filter and a line, not a machine-learning project.

Path 1 · recommended

Have your AI agent build it

get_matches covers the score half of this build well. Play-by-play — the drives, the down and distance, the per-play win probability — has no MCP tool, so an agent needs one direct REST call to get the rest of a game center.

1. Connect the MCP server

Add the server in your MCP client and sign in with your Big Balls account. Your client registers itself and handles the token exchange — there is no key to copy.

MCP client configjson
{
  "mcpServers": {
    "bigballs-sports-data": {
      "url": "https://mcp.bigballsdata.com/mcp"
    }
  }
}

2. Tools your agent gets

  • get_matches

    Live, upcoming or historical matches for a sport or league.

  • get_coverage

    Machine-readable map of what we hold and what we do not.

3. Ask for what you want

Paste this at your agent. It is written to make the model check coverage before it designs anything, which is what stops it inventing a field we do not serve.

Prompttext
Build me an NFL live game center for one game.

Use the Big Balls Sports Data MCP server's get_matches to find the game and
its current score and status.

The MCP server has no play-by-play tool, so for drives and win probability
call GET /v1/nfl/games/{game_id}/plays directly with my key — note this uses
the game's text id (e.g. "2025_01_DAL_PHI"), not the match UUID
get_matches returned; both point at the same game.

From the plays, build: the current drive (posteam, down, distance, field
position from the latest row) and a win-probability series across the game
(wp field, one point per play). Tell me which fields came from which call.

What the agent cannot reach

The two endpoints use two different identifiers for the same game (a match UUID from get_matches, a text game_id ("2025_01_DAL_PHI") for /plays), and nothing joins them automatically today. An agent has to carry both ids rather than assuming one resolves to the other.

Path 2 · hand-coded

Build it yourself

Every call below ran against production on 2026-09-11. Match lookup is free; NFL play-by-play requires Pro.

  1. 01

    Find a live or upcoming game

    Filter matches by status. A live game center only needs status=live; status=scheduled finds what is coming up.

    GET /v1/matchesbash
    curl -s "https://api.bigballsdata.com/v1/matches?sport=american_football&league=nfl&status=live" \
      -H "x-api-key: $BBS_API_KEY"

    Real response, trimmed

    jsonjson
    {
      "data": [
        {
          "id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668",
          "home": { "name": "Los Angeles Rams", "short_name": "LAR" },
          "away": { "name": "San Francisco 49ers", "short_name": "SF" },
          "kickoff_utc": "2026-09-11T00:35:00.000Z",
          "status": "live",
          "score": { "home": 7, "away": 10 },
          "linescore": { "away": [3, 7], "home": [0, 7] },
          "broadcast": "Netflix"
        }
      ],
      "meta": { "source": "stored" }
    }
  2. 02

    Poll the match for score changes

    GET /v1/matches/:id refreshes score and linescore for one game. Read meta.note before assuming push-frequency: this game was served "from the stored matches table (no live adapter currently lists this match)" at the moment above, which is an honest statement of polling cadence, not a defect.

    GET /v1/matches/:idbash
    curl -s "https://api.bigballsdata.com/v1/matches/5b2e8a2c-9c4f-4f0f-b035-fa23b0311668" \
      -H "x-api-key: $BBS_API_KEY"

    Real response, trimmed

    jsonjson
    {
      "data": { "id": "5b2e8a2c-9c4f-4f0f-b035-fa23b0311668", "status": "live", "score": { "home": 7, "away": 10 } },
      "meta": { "source": "stored", "note": "Served from the stored matches table (no live adapter currently lists this match)." }
    }
  3. 03

    Pull the drive from play-by-play

    This needs the game_id, not the match UUID: a text id like "2025_01_DAL_PHI". Every row carries down, distance, field position, the possessing team and a running win-probability value.

    GET /v1/nfl/games/:game_id/playsbash
    curl -s "https://api.bigballsdata.com/v1/nfl/games/2025_01_DAL_PHI/plays?limit=5" \
      -H "x-api-key: $BBS_API_KEY"

    Real response, trimmed

    jsonjson
    {
      "data": [
        {
          "play_id": "39", "game_id": "2025_01_DAL_PHI", "posteam": "SEA", "defteam": "SF",
          "play_type": "kickoff", "yards_gained": 0, "yardline_100": 35,
          "quarter_seconds_remaining": 900, "score_differential": 0,
          "epa": 0.562, "wp": 0.546
        }
      ],
      "pagination": { "total": 166 }
    }
  4. 04

    Build the win-probability chart

    wp is already computed per play — just sort by play_id and plot it. No model to run, no EPA-to-probability conversion to write.

    Win probability seriespython
    def wp_series(plays):
        return [
            {"play_id": p["play_id"], "wp": p["wp"], "clock": p["quarter_seconds_remaining"]}
            for p in sorted(plays, key=lambda p: int(p["play_id"]))
            if p["wp"] is not None
        ]
  5. 05

    Show the current drive

    The latest play with a non-null posteam and down tells you who has the ball, on what down, and where. Filter out kickoffs and administrative rows (play_type is null on the opening snap in this data) before picking "latest".

    Current drive from the last real snappython
    def current_drive(plays):
        live = [p for p in plays if p.get("down") is not None and p.get("posteam")]
        if not live:
            return None
        p = max(live, key=lambda p: int(p["play_id"]))
        return {
            "team": p["posteam"], "down": p["down"],
            "field_position": p["yardline_100"], "epa_last_play": p["epa"],
        }

Reference

Every endpoint this tutorial uses

All on the gateway at api.bigballsdata.com. Verified against production on 2026-09-11.

MethodPathReturnsWhy you need itPlan
GET/v1/matches?sport=american_football&league=nfl&status=Match id, teams, kickoff, status, score and linescoreThe scoreboard half of a game center — find live games, show the running score.Free
GET/v1/matches/:idOne match at a time: score, linescore, status, broadcastPoll this to refresh a single open game rather than re-fetching the whole live list.meta.note discloses when a game is served from the stored tier with no live adapter listing it — read it rather than assuming second-by-second push on every game.Free
GET/v1/nfl/games/:game_id/playsEvery play: down, distance, field position, EPA, win probability, possessionEverything a score feed cannot show — the drive underneath the number, and why the win probability moved.Pro

Pricing, honestly

Where the free tier stops

Match lookup is free, while the full NFL play-by-play feed with EPA and win probability requires Pro. Bookmaker odds and deeper historical analysis are additional paid capabilities.

Free key

jsonjson
{ "data": { "status": "live", "score": { "home": 7, "away": 10 } } }

Solo

jsonjson
{ "data": { "status": "live", "score": { "home": 7, "away": 10 }, "has_odds": true } }
  • Score and linescore are free; Pro unlocks full NFL play-by-play with EPA and win probability.
  • has_odds on a match response tells you whether a betting layer is available for that game; the odds themselves are Edge-plan (see the betting-model tutorial).

More tutorials

Other build guides

Questions

Is this actually live, or polled?
Polled, and the API says so rather than implying otherwise. /v1/matches/:id carries a meta.note stating when a game has no live adapter listing it and is being served from the stored tier instead. Build your polling interval around that disclosure rather than assuming push.
Why do matches and play-by-play use different ids for the same game?
get_matches (and every /v1/matches route) returns a canonical match UUID; /v1/nfl/games/:game_id/plays is keyed on a text game id, a string like "2025_01_DAL_PHI". Nothing joins them today, so carry both if your app needs both feeds for one game.
Do I need to compute win probability myself?
No. wp arrives precomputed on every play-by-play row, alongside epa. A chart is a sort and a filter, not a model.
Can an agent build this end to end with no direct HTTP calls?
Not quite. get_matches covers score and status. Drives, down-and-distance and win probability have no MCP tool yet — an agent has to call /v1/nfl/games/:game_id/plays directly with the same API key.

Build it

Start with a free key for match lookup, then use Pro for the full NFL play-by-play feed.