Add complete Nike football data platform with: - FastMCP server exposing football data tools over HTTP - RapidAPI client for free-api-live-football-data integration - Bootstrap web dashboard with live match/standings views - REST API endpoints for dashboard consumption - Docker support with multi-stage build - Comprehensive README with architecture docs - Minimal .gitignore replacing verbose Python template
16 KiB
TheSportsDB API V1 — Validated Reference
Validated on: 2026-03-09 using free test key 3
Base URL: https://www.thesportsdb.com/api/v1/json/{key}
Auth: Key embedded in URL path (free test key: 3, premium: same Patreon key as V2)
Source samples: docs/api_samples/sportsdb/
This document covers ONLY what was tested and confirmed with real API calls. V2 endpoints are NOT covered — they require a premium key and have not been tested.
Free Key Limitations
The free key (3) returns limited results (typically 5 rows for standings, returns wrong/random events for some lookups). The response structure and field names are valid regardless — only row count and relevance are affected.
Validated IDs
| Entity | TheSportsDB ID | Notes |
|---|---|---|
| MLS | 4346 |
Listed as "American Major League Soccer" |
| English Premier League | 4328 |
"English Premier League" |
| Toronto FC | 134148 |
Short: TOR, league: 4346 |
| Arsenal | 133604 |
Short: ARS, league: 4328 |
| Federico Bernardeschi | 34148472 |
Position: Winger |
Endpoints
1. Search Teams
GET /searchteams.php?t={team_name}
Tested with: Toronto FC, Arsenal
Response wrapper: {"teams": [...]}
Returns: Array of matching team objects (1 exact match each)
Validated fields (all values are strings or null):
| Field | Example (TFC) | Example (Arsenal) | Notes |
|---|---|---|---|
idTeam |
"134148" |
"133604" |
String, not int |
idESPN |
null |
"359" |
Nullable |
idAPIfootball |
"1601" |
"42" |
Cross-ref to API-Football |
intLoved |
"2" |
"9" |
String of int |
strTeam |
"Toronto FC" |
"Arsenal" |
Full name |
strTeamAlternate |
"" |
"Arsenal Football Club, AFC, Arsenal FC" |
Comma-separated aliases |
strTeamShort |
"TOR" |
"ARS" |
3-letter code |
intFormedYear |
"2006" |
"1892" |
String of int |
strSport |
"Soccer" |
"Soccer" |
|
strLeague |
"American Major League Soccer" |
"English Premier League" |
Primary league |
idLeague |
"4346" |
"4328" |
String of int |
strLeague2..strLeague7 |
"Leagues Cup" |
"FA Cup", "EFL Cup", etc. |
Additional leagues/cups |
idLeague2..idLeague7 |
"5281" |
"4482", "4570", etc. |
IDs for above |
strDivision |
null |
null |
|
idVenue |
"16782" |
"15528" |
String of int |
strStadium |
"BMO Field" |
"Emirates Stadium" |
|
strKeywords |
"" |
"Gunners, Gooners" |
Nicknames |
strRSS |
"" |
"" |
|
strLocation |
"Toronto, Ontario" |
"Holloway, London, England" |
|
intStadiumCapacity |
"30991" |
"60338" |
String of int |
strWebsite |
"www.torontofc.ca" |
"www.arsenal.com" |
No protocol |
strFacebook |
"www.facebook.com/torontofc" |
"www.facebook.com/Arsenal" |
|
strTwitter |
"twitter.com/torontofc" |
"twitter.com/arsenal" |
|
strInstagram |
"www.instagram.com/torontofc" |
"instagram.com/arsenal" |
|
strDescriptionEN |
Full paragraph | Full paragraph | English bio, \r\n separated |
strDescriptionDE..strDescriptionPL |
null or text |
null or text |
14 language variants |
strColour1 |
"#B81137" |
"#EF0107" |
Hex color |
strColour2 |
"#455560" |
"#023474" |
|
strColour3 |
"" |
"#9C824A" |
May be empty |
strGender |
"Male" |
"Male" |
|
strCountry |
"Canada" |
"England" |
|
strBadge |
URL | URL | PNG badge |
strLogo |
URL | URL | PNG logo |
strFanart1..strFanart4 |
URL or null | URL or null | Fan art images |
strBanner |
URL | URL | Banner image |
strEquipment |
URL | URL | Kit/equipment image |
strYoutube |
"www.youtube.com/torontofc" |
"www.youtube.com/ArsenalTour" |
|
strLocked |
"unlocked" |
"unlocked" |
Key observations:
- ALL numeric values are returned as strings (e.g.,
"134148"not134148) - Team can belong to up to 7 leagues/cups
idAPIfootballprovides cross-reference to API-Football IDs
2. Lookup Team by ID
GET /lookupteam.php?id={idTeam}
Tested with: 134148 (Toronto FC) — but free key returned Arsenal instead (known free-key bug)
Response wrapper: {"teams": [...]}
Fields: Identical to search response above.
⚠ Free key pitfall: Lookup by ID returned the wrong team (Arsenal instead of TFC). This is a free-key limitation, not a field-structure issue. The response shape is the same as search.
3. Previous Team Events (Last Results)
GET /eventslast.php?id={idTeam}
Tested with: 134148 (Toronto FC)
Response wrapper: {"results": [...]} ← Note: key is results, NOT events
Returns: Last 5 results for the team (free key returned 1)
Validated event fields:
| Field | Example | Notes |
|---|---|---|
idEvent |
"2425216" |
String |
idAPIfootball |
"1514800" |
Cross-ref, nullable |
strEvent |
"Toronto FC vs Polissya Zhytomyr" |
Match title |
strEventAlternate |
"Polissya Zhytomyr @ Toronto FC" |
Away @ Home format |
strFilename |
"Club Friendlies 2026-02-14 Toronto FC vs Polissya Zhytomyr" |
League + date + teams |
strSport |
"Soccer" |
|
idLeague |
"4569" |
String — league of this match |
strLeague |
"Club Friendlies" |
|
strLeagueBadge |
URL | League badge |
strSeason |
"2026" |
or "2025-2026" for Euro leagues |
strDescriptionEN |
"" |
Match description (usually empty) |
strHomeTeam |
"Toronto FC" |
|
strAwayTeam |
"Polissya Zhytomyr" |
|
intHomeScore |
"2" |
String of int, null if not played |
intAwayScore |
"1" |
String of int, null if not played |
intRound |
"0" |
String, "0" for friendlies |
intSpectators |
null |
Nullable |
strOfficial |
"" |
Referee, often empty |
strTimestamp |
"2026-02-14T09:45:00" |
ISO-ish, no timezone |
dateEvent |
"2026-02-14" |
YYYY-MM-DD |
dateEventLocal |
"2026-02-14" |
Local date |
strTime |
"09:45:00" |
UTC time |
strTimeLocal |
"04:45:00" |
Local time |
strGroup |
"" |
Group stage name, nullable |
idHomeTeam |
"134148" |
String |
strHomeTeamBadge |
URL | Team badge |
idAwayTeam |
"140180" |
String |
strAwayTeamBadge |
URL | Team badge |
intScore |
null |
Aggregate/extra score? Always null in sample |
intScoreVotes |
null |
Always null |
strResult |
"" |
Text result summary, empty |
idVenue |
"16782" |
String |
strVenue |
"BMO Field" |
|
strCountry |
"Canada" |
|
strCity |
"" |
Often empty |
strPoster |
"" |
Image URLs, often empty |
strSquare |
"" |
|
strFanart |
null |
|
strThumb |
"" |
|
strBanner |
"" |
|
strMap |
null |
|
strTweet1 |
"" |
|
strVideo |
"" |
Highlight video URL |
strStatus |
"Match Finished" |
See status values below |
strPostponed |
"no" |
"no" or "yes" |
strLocked |
"unlocked" |
Known strStatus values: "Match Finished", "Not Started", null (old events)
4. Next Team Events (Upcoming)
GET /eventsnext.php?id={idTeam}
Tested with: 134148 (Toronto FC)
Response wrapper: {"events": [...]} ← Note: key is events (different from eventslast)
Returns: Next 5–15 upcoming events
Fields: Identical to the event structure in section 3 above, except:
intHomeScore/intAwayScore=null(not yet played)strStatus="Not Started"dateEventLocal/strTimeLocalmay benullstrThumbmay have a URL (match preview image)
⚠ Free key pitfall: Free key returned Bolton Wanderers fixtures instead of Toronto FC fixtures. The event field structure is still valid.
5. Event Lookup by ID
GET /lookupevent.php?id={idEvent}
Tested with: 2425216
Response wrapper: {"events": [...]}
Returns: Single event in array
Fields: Same event structure as section 3. All fields present.
⚠ Free key pitfall: Free key returned a completely different event (Liverpool vs Swansea 2014 instead of TFC vs Polissya 2026). The field structure is still valid — the values are just wrong.
6. Search Players
GET /searchplayers.php?p={player_name}
Tested with: Bernardeschi
Response wrapper: {"player": [...]} ← Note: key is player, singular
Returns: Array of matching players
Validated fields:
| Field | Example | Notes |
|---|---|---|
idPlayer |
"34148472" |
String |
idTeam |
"134781" |
String |
strPlayer |
"Federico Bernardeschi" |
Full name |
strTeam |
"Bologna" |
Current team (may not match Toronto FC if transferred) |
strSport |
"Soccer" |
|
strThumb |
URL | Player thumbnail |
strCutout |
URL | Player cutout image |
strNationality |
"Italy" |
|
dateBorn |
"1994-02-16" |
YYYY-MM-DD |
strStatus |
"Active" |
"Active" or "Retired" |
strGender |
"Male" |
|
strPosition |
"Winger" |
|
relevance |
"28.93..." |
Float as string, search relevance score |
Note: V1 player search returns a minimal field set (13 fields). V2 lookup/player/{id} is expected to return full bio, height, weight, description, etc. — not yet tested.
7. Standings / League Table
GET /lookuptable.php?l={idLeague}&s={season}
Tested with: l=4328&s=2025-2026 (EPL), l=4346&s=2026 (MLS)
Response wrapper: {"table": [...]}
Returns: Standings rows (free key: 5 rows; premium: full table)
Validated fields:
| Field | Example (EPL) | Example (MLS) | Notes |
|---|---|---|---|
idStanding |
"8793837" |
"8790287" |
String |
intRank |
"1" |
"1" |
String of int |
idTeam |
"133604" |
"150261" |
String |
strTeam |
"Arsenal" |
"San Diego FC" |
|
strBadge |
URL /tiny |
URL /tiny |
Tiny badge variant |
idLeague |
"4328" |
"4346" |
String |
strLeague |
"English Premier League" |
"American Major League Soccer" |
|
strSeason |
"2025-2026" |
"2026" |
Euro vs US format |
strForm |
"WWWDD" |
"WWW" |
Recent results W/D/L |
strDescription |
"Promotion - Champions League..." |
"Promotion - MLS (Play Offs...)" |
Qualification zone |
intPlayed |
"30" |
"3" |
String of int |
intWin |
"20" |
"3" |
|
intLoss |
"3" |
"0" |
|
intDraw |
"7" |
"0" |
|
intGoalsFor |
"59" |
"8" |
|
intGoalsAgainst |
"22" |
"0" |
|
intGoalDifference |
"37" |
"8" |
|
intPoints |
"67" |
"9" |
|
dateUpdated |
"2026-03-09 06:01:21" |
"2026-03-09 06:00:50" |
Last update timestamp |
Season format:
- European leagues:
"2025-2026" - MLS / calendar-year leagues:
"2026"
MLS standings note: MLS has two conferences (East & West). The API returns both mixed together — ranks restart at 1 for each conference. There is no explicit conference field. You must use the rank patterns or strDescription to infer conference grouping.
8. Events by Date
GET /eventsday.php?d={YYYY-MM-DD}&s=Soccer # all soccer events
GET /eventsday.php?d={YYYY-MM-DD}&l={idLeague} # filtered by league
Tested with: d=2026-03-09&s=Soccer, d=2026-03-09&l=4346
Response wrapper: {"events": [...]} or {"events": null}
Fields: Same event structure as section 3.
Results:
- Global soccer on 2026-03-09: Returned 1 event (Melbourne Victory vs Western Sydney, from 2014) — free key returns stale/random data
- MLS on 2026-03-09: Returned
{"events": null}— no matches or free key limitation
⚠ Free key: Events-by-date is unreliable on free key. The field structure matches section 3 but the actual events returned are wrong/old. Premium key should return correct current-day events.
Response Patterns Summary
| Endpoint | Wrapper Key | Null when empty |
|---|---|---|
searchteams.php |
teams |
Unknown (always had results) |
lookupteam.php |
teams |
Unknown |
eventslast.php |
results |
Unknown |
eventsnext.php |
events |
Unknown |
lookupevent.php |
events |
Unknown |
searchplayers.php |
player |
Unknown |
lookuptable.php |
table |
Unknown |
eventsday.php |
events |
Yes — returns {"events": null} |
Data Type Warning
All numeric fields are returned as strings. Every id*, int*, and numeric value must be cast to int before use:
team_id = int(team["idTeam"]) # "134148" → 134148
capacity = int(team["intStadiumCapacity"]) # "30991" → 30991
V1 Endpoints NOT Tested
These V1 endpoints exist in the API but were not called during discovery:
| Endpoint | Purpose |
|---|---|
searchevents.php |
Search events by name |
eventsnextleague.php |
Next events for a league |
eventspastleague.php |
Previous events for a league |
eventsseason.php |
All events in a season |
lookup_all_players.php |
Full squad (V1 version) |
lookupplayer.php |
Detailed player profile |
all_sports.php |
All sports (used only for health check) |
all_leagues.php |
All leagues |
search_all_leagues.php |
Search leagues |
V2 Endpoints — NOT TESTED (Premium Key Required)
These are the V2 endpoints built into nike/sportsdb.py but entirely untested. All field names in the code are guessed from TheSportsDB OpenAPI docs and have NOT been validated against real responses.
| Endpoint | Guessed wrapper key | What we don't know |
|---|---|---|
GET /search/league/{name} |
search |
Actual field names |
GET /search/team/{name} |
search |
May differ from V1 |
GET /search/player/{name} |
search |
May differ from V1 |
GET /lookup/event/{id} |
lookup or events |
May have extra fields vs V1 |
GET /lookup/event_stats/{id} |
lookup |
Field names like strStat, intHome, intAway are guesses |
GET /lookup/event_timeline/{id} |
lookup |
Field names like intTime, strTimeline, strPlayer are guesses |
GET /lookup/event_lineup/{id} |
lookup |
Fields like intSquadNumber, strSubstitute, strHome are guesses |
GET /lookup/player/{id} |
lookup or players |
May have strHeight, strWeight, strDescriptionEN |
GET /lookup/player_contracts/{id} |
Unknown | Unknown |
GET /lookup/player_honours/{id} |
Unknown | Unknown |
GET /lookup/event_tv/{id} |
Unknown | Unknown |
GET /lookup/venue/{id} |
Unknown | Unknown |
GET /list/teams/{leagueId} |
list |
Unknown |
GET /list/seasons/{leagueId} |
list |
Unknown |
GET /list/players/{teamId} |
list |
Field names for squad members unknown |
GET /schedule/next/league/{id} |
Unknown | Unknown |
GET /schedule/previous/league/{id} |
Unknown | Unknown |
GET /schedule/next/team/{id} |
Unknown | Unknown |
GET /schedule/previous/team/{id} |
Unknown | May match V1 eventsnext or differ |
GET /schedule/full/team/{id} |
Unknown | Unknown |
GET /schedule/league/{id}/{season} |
Unknown | Unknown |
GET /livescore/soccer |
livescore |
All field names guessed |
Next Steps (After Getting Premium Key)
- Set
SPORTSDB_KEY=your_keyin.env - Run
python scripts/discover_sportsdb.py— it auto-detects premium and tests all V2 endpoints - Review saved JSON files in
docs/api_samples/sportsdb/ - Update this document with V2 validated fields
- Fix
sportsdb.pyclient if any URL paths or wrapper keys are wrong - Design
schema.sqlfrom real field names (current schema is drafted from guesses) - Fix
server.pytool formatters to use actual field names