College football API in Python: scores, rankings, box scores and play-by-play

A single Python script with four commands: the current scoreboard or any day's games (filter by team), the AP Top 25, coaches poll or CFP rankings, passing/rushing/receiving lines from one game's box score, and a game's play-by-play with the score after each play.

Stack
Python, requests
Plan
Free (each command is one or two calls)

Updated 2026-11-05. Tested against the live API on that date. Data is aggregated from public sources and is typically 20-30 seconds behind live play (about 1 second after our source). There is no SLA.

1. Get a free API key

Sign up (no card) and copy the key from the dashboard. The Free plan gives you 1,000 calls in the first 30 days, then 125 a month, at 1 request per second. Every successful REST call counts as one call.

Check the key works with one call to the live-scores endpoint:

Terminal

export REALTIME_SPORTS_API_KEY=your_key
curl -H "Authorization: Bearer $REALTIME_SPORTS_API_KEY" \
  "https://www.realtimesportsapi.com/api/v1/sports/football/leagues/nfl/events/live"

2. Get the code

Everything below is in the python-college-football/ folder of the examples repo (MIT). Clone it and work from that folder.

Terminal

git clone https://github.com/ElcoDevRepos/realtime-sports-api-examples
cd realtime-sports-api-examples/python-college-football

3. Install

Python 3.10+ and requests. Nothing else.

Terminal

python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env    # REALTIME_SPORTS_API_KEY

4. The script

api_get sends your key as a Bearer token and stops with the API's own error message if a call fails. scores reads the current scoreboard, or one US Eastern day with --date (a full Saturday is dozens of games, so --team UGA filters by abbreviation). box walks each team's players and prints the passing, rushing and receiving categories. plays reads the play-by-play, oldest first and 100 per page: by default it fetches the last page, which is the end of the game (or the latest plays of a live one); --all walks every page. rankings prints a poll (AP by default, --poll coaches or cfp) with each team's id, record, movement and first-place votes; --week reads a past week.

python-college-football/cfb.py

#!/usr/bin/env python3
"""College football (NCAA FBS) data in Python with the Realtime Sports API: scores, box scores and play-by-play.

Usage:
  python cfb.py scores                          # the current scoreboard (this week's games)
  python cfb.py scores --date 2026-10-31        # one day's games (Saturdays are big: use --team to filter)
  python cfb.py scores --date 2026-10-31 --team UGA
  python cfb.py box <eventId>                   # passing, rushing and receiving lines for one game
  python cfb.py plays <eventId> --last 15       # the last plays of a game, with the score after each
  python cfb.py rankings                        # AP Top 25 (or --poll coaches / cfp), latest week
  python cfb.py rankings --poll ap --week 5     # a past week of a poll

Each command is one API call (plays: two when the game has more than 100 plays; --all fetches every page). Needs REALTIME_SPORTS_API_KEY (env or .env).
"""
from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path
from typing import Any, Dict, List

import requests

API_BASE = os.environ.get("RSA_API_BASE", "https://www.realtimesportsapi.com/api/v1")
CFB = "/sports/football/leagues/college-football"


def load_dotenv(path: Path) -> None:
    """Tiny .env loader (KEY=value). Existing environment variables win."""
    if not path.exists():
        return
    for line in path.read_text().splitlines():
        line = line.strip()
        if line and not line.startswith("#") and "=" in line:
            k, v = line.split("=", 1)
            os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))


def api_get(path: str, **params: Any) -> Dict[str, Any]:
    key = os.environ.get("REALTIME_SPORTS_API_KEY")
    if not key:
        sys.exit("Set REALTIME_SPORTS_API_KEY (get a free key at https://www.realtimesportsapi.com/signup)")
    r = requests.get(f"{API_BASE}{path}", params=params, headers={"Authorization": f"Bearer {key}"}, timeout=20)
    body = r.json()
    if r.status_code != 200 or not body.get("success", False):
        err = body.get("error") or {}
        sys.exit(f"HTTP {r.status_code}: {err.get('message') or body}")
    return body


def team_line(t: Dict[str, Any], show_score: bool = True) -> str:
    score = t.get("score") if show_score else None
    return f"{t.get('abbreviation') or t.get('name')}{'' if score is None else f' {score}'}"


def cmd_scores(args: argparse.Namespace) -> None:
    params: Dict[str, Any] = {"limit": 200}
    if args.date:
        params["date"] = args.date
    games: List[Dict[str, Any]] = api_get(f"{CFB}/events", **params)["data"]
    if args.team:
        want = args.team.upper()
        games = [g for g in games if want in ((g["awayTeam"].get("abbreviation") or "").upper(), (g["homeTeam"].get("abbreviation") or "").upper())]
    for g in sorted(games, key=lambda e: e.get("date") or ""):
        st = g.get("status") or {}
        started = st.get("state") != "pre"  # no 0-0 for games that haven't kicked off
        print(f"[{g['id']}] {team_line(g['awayTeam'], started)} @ {team_line(g['homeTeam'], started)}  {st.get('detail') or st.get('state')}")
    print(f"\n{len(games)} games")


def cmd_box(args: argparse.Namespace) -> None:
    box = api_get(f"{CFB}/events/{args.event_id}/boxscore")["data"] or {}
    for side in ("away", "home"):
        team = box.get(f"{side}Team") or {}
        print(f"\n{team.get('name', side)} {team.get('score', '')}")
        for p in box.get(f"{side}Players") or []:
            c = p.get("categories") or {}
            parts = []
            if "passing" in c:
                ps = c["passing"]
                parts.append(f"{ps.get('completions/passingAttempts')} for {ps.get('passingYards')} yds, {ps.get('passingTouchdowns')} TD, {ps.get('interceptions')} INT")
            if "rushing" in c:
                rs = c["rushing"]
                parts.append(f"{rs.get('rushingAttempts')} rush, {rs.get('rushingYards')} yds, {rs.get('rushingTouchdowns')} TD")
            if "receiving" in c:
                rc = c["receiving"]
                parts.append(f"{rc.get('receptions')} rec, {rc.get('receivingYards')} yds, {rc.get('receivingTouchdowns')} TD")
            if parts:
                print(f"  {p.get('name')}: " + "; ".join(parts))


def cmd_plays(args: argparse.Namespace) -> None:
    """Plays come oldest first, 100 per page. Default: the game's last page (the end of the game, or the
    latest plays if it is live). --all walks every page."""
    path = f"{CFB}/events/{args.event_id}/plays"
    first = api_get(path, limit=100, page=1)
    pg = (first.get("meta") or {}).get("pagination") or {}
    pages = pg.get("totalPages") or 1
    plays: List[Dict[str, Any]] = list(first["data"] or [])
    if args.all:
        for page in range(2, pages + 1):
            plays.extend(api_get(path, limit=100, page=page)["data"] or [])
    elif pages > 1:
        plays = list(api_get(path, limit=100, page=pages)["data"] or [])
    for p in plays[-args.last:] if args.last else plays:
        period = (p.get("period") or {}).get("number") if isinstance(p.get("period"), dict) else p.get("period")
        clock = (p.get("clock") or {}).get("displayValue") if isinstance(p.get("clock"), dict) else (p.get("clock") or "")
        score = f"{p.get('awayScore', '')}-{p.get('homeScore', '')}"
        print(f"Q{period} {clock or '':>5}  {score:>7}  {p.get('text', '')}")
    print(f"\n{pg.get('total', len(plays))} plays in the game")


def cmd_rankings(args: argparse.Namespace) -> None:
    params: Dict[str, Any] = {"poll": args.poll}
    if args.week:
        params["week"] = args.week
    for poll in api_get(f"{CFB}/rankings", **params)["data"]:
        print(f"\n{poll['name']} - {poll['week'].get('label') or ''}")
        for r in poll["ranks"]:
            t = r["team"]
            move = "" if r.get("trend") in (None, "-") else f" ({r['trend']})"
            votes = f", {r['firstPlaceVotes']} first-place" if r.get("firstPlaceVotes") else ""
            print(f"  {r['rank']:>2}. {t.get('displayName')} [{t.get('id')}] {r.get('record') or ''}{move}{votes}")


def main() -> None:
    load_dotenv(Path(__file__).with_name(".env"))
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)
    s = sub.add_parser("scores")
    s.add_argument("--date", help="YYYY-MM-DD, one US Eastern day (default: the current scoreboard)")
    s.add_argument("--team", help="filter by team abbreviation, e.g. UGA")
    s.set_defaults(fn=cmd_scores)
    b = sub.add_parser("box")
    b.add_argument("event_id")
    b.set_defaults(fn=cmd_box)
    pl = sub.add_parser("plays")
    pl.add_argument("event_id")
    pl.add_argument("--last", type=int, default=15, help="show only the last N plays (0 = all fetched)")
    pl.add_argument("--all", action="store_true", help="fetch every page, not just the first 100 plays")
    pl.set_defaults(fn=cmd_plays)
    rk = sub.add_parser("rankings")
    rk.add_argument("--poll", default="ap", choices=["ap", "coaches", "cfp"])
    rk.add_argument("--week", type=int, help="a past week of this season (default: latest)")
    rk.set_defaults(fn=cmd_rankings)
    args = ap.parse_args()
    args.fn(args)


if __name__ == "__main__":
    main()

5. Run it

On 5 November 2026, scores --date 2026-10-31 returned 47 games and --team UGA narrowed it to [401856734] FLA 8 @ UGA 27 Final. box 401856734 printed lines like Gunner Stockton: 13/23 for 101 yds, 0 TD, 0 INT; 12 rush, 45 yds, 0 TD and Nate Frazier: 12 rush, 67 yds, 1 TD. plays 401856734 --last 8 showed Georgia running out the clock, ending in a turnover on downs with 16 seconds left; the game had 164 plays. With no --date, scores listed this week's scoreboard, from Tuesday's finals (OHIO 28 @ AKR 20) to Saturday's kickoffs. rankings printed the Week 10 AP Top 25 headed by 1. Miami Hurricanes [2390] 8-0, 62 first-place, and --poll cfp the committee's first rankings of the season, also led by Miami.

Terminal

python cfb.py scores                               # the current scoreboard
python cfb.py scores --date 2026-10-31 --team UGA  # one day, one team
python cfb.py box 401856734                        # one game's stat lines
python cfb.py plays 401856734 --last 15            # how it ended, play by play
python cfb.py rankings --poll cfp                  # AP by default; coaches or cfp

6. Going further

For the NFL version (with injury reports), see NFL API in Python. For a full season in pandas, see the season analysis guide. To react to scores without polling, use webhooks or the WebSocket stream on a paid plan. Data typically runs 20-30 seconds behind live play. See every college football endpoint on the college football API page.

Endpoints used

  • GET /sports/football/leagues/college-football/events?date=YYYY-MM-DD
  • GET /sports/football/leagues/college-football/rankings
  • GET /sports/football/leagues/college-football/events/{eventId}/boxscore
  • GET /sports/football/leagues/college-football/events/{eventId}/plays

Full reference: docs · OpenAPI · what each endpoint returns, by league

FAQ

Is there a free college football API for Python?
Yes. Any HTTP client works; this guide uses requests. A free key (no card) gives 1,000 calls in the first 30 days, then 125 a month, and includes every endpoint, including box scores and play-by-play.
How do I get every college football game on a Saturday?
Call GET /sports/football/leagues/college-football/events?date=YYYY-MM-DD&limit=200. The date is one US Eastern day; `python cfb.py scores --date ...` does exactly that.
Does it include college football rankings or polls?
Yes. GET /sports/football/leagues/college-football/rankings returns the AP Top 25, the coaches poll and the College Football Playoff rankings (filter with poll=ap, coaches or cfp). Each ranked team carries its id, so you can join it to the scores this script prints.

More guides