ESPN Provider
ESPN is the primary data provider (priority 0), serving 106 pre-configured leagues plus ~228 dynamically discovered soccer leagues. The API is free, public, and requires no authentication.
API Details
| Base URL | https://site.api.espn.com/apis/site/v2/sports |
| Auth | None required |
| Rate Limit | Generous (practically impossible to hit — DNS throttling is the usual bottleneck) |
Key Endpoints
| Endpoint | Description |
|---|---|
/{sport}/{league}/scoreboard?dates={YYYYMMDD} | Games on a specific date |
/{sport}/{league}/teams/{team_id}/schedule | Team schedule |
/{sport}/{league}/teams/{team_id} | Team details |
/{sport}/{league}/summary?event={event_id} | Event details and scores |
/{sport}/{league}/teams | All teams in a league (cache refresh) |
/{sport}/{league}/rankings | Poll rankings — college leagues only |
/cricket/{series_id}/scoreboard?season={YYYY} | Complete franchise season and team list |
HTTP Client Configuration
| Setting | Default | Env Variable |
|---|---|---|
| Max connections | 100 | ESPN_MAX_CONNECTIONS |
| Timeout | 10s | ESPN_TIMEOUT |
| Retry count | 3 | ESPN_RETRY_COUNT |
| Max workers | 100 (team processing) / 50 (cache refresh) / 24 (event prefetch) | ESPN_MAX_WORKERS |
ESPN_MAX_WORKERS is read by the consumers, not the HTTP client, and has three defaults: 100 in the team processor, 50 in cache refresh (lower because refresh makes more API calls per league), and 24 in the event-match prefetch (which fans a league x date window out across threads). Setting the env var overrides all three.
Retry logic uses exponential backoff: 0.5s → 1s → 2s → 4s (capped at 10s) with ±30% jitter. Rate limit (429) responses trigger longer backoff: 5s → 10s → 20s (capped at 60s), respecting the Retry-After header if present.
League ID Format
ESPN leagues are configured in schema.sql with provider_league_id in sport/league format:
football/nfl
basketball/nba
hockey/nhl
soccer/eng.1
baseball/mlb
Sports Coverage
| Sport | Leagues | Notes |
|---|---|---|
| Football | NFL, NCAAF, UFL | |
| Basketball | NBA, WNBA, G League, NCAAM, NCAAW, NBL (Australia) | FIBA World Cup (M/W) is TSDB — ESPN only tracks the final, not qualifiers |
| Hockey | NHL, NCAA M/W, Olympics M/W | |
| Baseball | MLB, NCAA Baseball, World Baseball Classic, Little League Baseball | MiLB handled by MLB Stats provider; LLB is the August World Series only |
| Soccer | 48 pre-configured, ~228 discovered | Dot notation: eng.1, ger.2 |
| Rugby | 20 leagues (Six Nations, Rugby World Cup, Super Rugby, URC, Premiership, Top 14, MLR, NRL, Olympic 7s, …) | Second-largest ESPN sport by league count; NRL and Super Rugby Pacific migrated here from TSDB |
| Combat Sports | UFC, PFL | Event Card matching. ESPN files every other promotion (Cage Warriors, OKTAGON, RIZIN, ONE, and LFA since June 2026) under a catch-all mma/other bucket, and only after the event, so none of them can be a league (#760, #1083) |
| Motorsports | F1, IndyCar | Race weekend sessions |
| Tennis | ATP, WTA | One event per match; grand slams split by draw type |
| Cricket | ODIs, T20 Internationals, Test Matches; IPL, BBL, SA20, MLC | Internationals filter the daily score panel by format (#944). Franchise leagues use recurring numeric series IDs and complete season scoreboards, including team import and team channels (#1069) |
| Lacrosse | NLL, PLL, NCAA M/W | |
| Volleyball | NCAA M/W | |
| Softball | NCAA Softball |
Soccer League Discovery
ESPN’s API exposes a couple hundred soccer leagues through its /v2/sports/soccer/leagues discovery endpoint (Teamarr requests limit=500; ~228 discovered leagues is typical after filtering). During cache refresh, Teamarr discovers available leagues and makes them selectable in the league picker under the Soccer sport. These discovered leagues support event matching in event groups but don’t have pre-configured team import. A small number of real soccer leagues (e.g. Swiss Super League, Israeli Premier League) are omitted from ESPN’s discovery index despite being fully served by the data endpoints — these are registered as primary leagues in schema.sql as a workaround.
Soccer leagues use ESPN’s dot notation: {country}.{tier} (e.g., eng.1 for Premier League, ger.2 for 2. Bundesliga).
Rugby uses numeric slug IDs instead of string slugs — rugby/180659 (Six Nations), rugby/242041 (Super Rugby) — and rugby league is a separate ESPN sport path: rugby-league/3 (NRL).
Special Behaviors
Franchise cricket
IPL (cricket/8048), BBL (cricket/8044), SA20 (cricket/21275) and MLC (cricket/21266) require no API key. Their team list, team lookup, schedules, history and previews come from scoreboards, not ESPN’s unsupported cricket team endpoints. A season response is shared for five minutes across day/team readers; failed season requests have a 30-second retry cooldown. International format leagues keep their daily-panel behavior and do not offer team import.
An explicit season is essential: a date alone silently searches the default season. BBL uses its starting year (2026 includes January 2027); SA20’s labels are irregular (2025/26, then 2027, with no 2026 label). Teamarr reads the default series link and nearby season years, deduplicates matches and uses their UTC timestamps with the usual user-timezone day filtering. Cricket numeric scores and team statistics remain unavailable; result text is retained.
On upgrade, existing league subscriptions stay selected. Recognized TSDB teams, team filters, aliases, priorities and numbering pins move to ESPN IDs without changing local team IDs, XMLTV IDs, custom logos or durable team-channel ownership. Unknown or duplicate team configurations are preserved with a migration warning; review and reselect those entries in the team picker rather than removing an exclude filter. Old event channels retain their original ownership until normal retirement. Pinned manual stream corrections are kept; re-pin a correction to an ESPN fixture if it still references an old TSDB event.
Shared behaviors
- Status mapping: ESPN event statuses are normalized to Teamarr’s internal
scheduled,in_progress,final,postponed,cancelled - Cricket statuses carry no status name, only a state and a description (“Stumps”, “Result”, “No result”, “Abandoned”); Teamarr maps those directly (stumps and rain delays are a match in progress; a match marked live whose summary still says “Starts at” is scheduled). Cricket scores are innings text and are not parsed; the match label (“3rd ODI”) and the result text (“India won by 14 runs”) are kept instead. (#944)
- Season type normalization: ESPN’s
season.slugfield is parsed to canonicalpreseason/regular/postseason/offseasonvalues. The slug is the primary source (handles soccer knockouts:semifinals,round-of-16,final, etc.), falling back to the numericseason.type(1–4) for leagues where slug is absent. The summary endpoint (/summary?event=) nestsseasonunderheader.season, soget_eventpasses it through explicitly — otherwise a refresh would wipe the season_type set during the initial scoreboard fetch. - Team ID corrections: Hardcoded mapping for known ESPN data mismatches (e.g., some women’s hockey teams)
- NCAA scoreboards: NCAA leagues whose ungrouped ESPN scoreboard omits subdivisions fetch and merge ESPN’s division groups. This includes FBS, FCS, lower-division, and cross-division fixtures when ESPN lists them. Teamarr cannot match fixtures ESPN does not expose in a public scoreboard group. College football, both college basketballs and women’s volleyball can drop a division from the fetch entirely — see Divisions under Per-League Channel Config. Cross-division games are filed under both divisions by ESPN, so they survive either selection; Women’s volleyball and women’s lacrosse also pull one conference group each (United Athletic, Mid-American) that ESPN leaves out of the division slate; those are conferences rather than divisions, so they are always fetched.
- Tournament sports: Racing events have no home/away teams — parsed via
TournamentParserMixin. (Golf is wired into the same code path but no golf leagues are currently seeded.) Tennis is the exception:TennisParserMixinexpands each tournament into one Event per MATCH, with the two players as home/away teams (surname as abbreviation). Grand slams appear on both the atp and wta endpoints, so each league keeps only its own draw types (atp: men’s + mixed doubles; wta: women’s). Tennis quirks: ESPN ignores?dates=YYYYMMDDfor tennis and returns whole tournaments overlapping the window, so Teamarr slices per-day client-side; scoreboardathlete.idis null, so tennis team ids are name-derived and matching is name-based; doubles competitors carry onlyroster.displayName(“A / B”), no athlete objects;venue.courtcan be empty (walkovers/unassigned) and qualifying courts are named “Court N Roehampton”, both tolerated by court mapping. - Poll rankings: ESPN’s team payload carries no rank field for any league, so
TeamStats.rankcomes from the league’s/rankingspolls, fetched once per league and cached for 6 hours. Only college leagues publish polls (pro leagues 404, and the endpoint is not called for them). Every live poll in the payload is merged, AP first, so an FBS team gets its AP rank while FCS and Division II teams get theirs from their own polls. Tournament seedings are ignored (that isplayoff_seed), and a poll older than 45 days is treated as an ended season — ESPN keeps serving a season’s final poll all offseason, so without that cutoff last season’s ranks would appear on this season’s listings. - UFC: Parsed via
UFCParserMixinwith fighter name extraction from the core API
File Locations
| File | Purpose |
|---|---|
teamarr/providers/espn/provider.py | ESPNProvider class |
teamarr/providers/espn/client.py | HTTP client with retry logic; ESPN_TEAM_ID_CORRECTIONS (team ID corrections) |
teamarr/providers/espn/constants.py | Status mapping |
teamarr/providers/espn/tournament.py | TournamentParserMixin (racing; golf code path unused) |
teamarr/providers/espn/editorial_canary.py | Drift canary for editorial fields (headlines, notes, altGameNote, neutralSite) |
teamarr/providers/espn/tennis.py | TennisParserMixin (per-match events, draw-type split) |
teamarr/providers/espn/ufc.py | UFCParserMixin |