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)
- Code
- python-college-football/ (MIT)
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-football3. 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_KEY4. 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 cfp6. 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-DDGET /sports/football/leagues/college-football/rankingsGET /sports/football/leagues/college-football/events/{eventId}/boxscoreGET /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.