Scope
Teams/leagues APIs provide team metadata, league metadata, sports catalog, join vs follow, and sync-connected behavior used by web and native clients.Teams endpoints
GET /api/teams— list, or?slug=for a single teamGET /api/teams/{id}— public fetch by id or slug (id is tried first). Same team shape as a list entry, plusteamRecord,foundedDate, andviewerCanPostAsTeam. Archived teams return404.GET /api/teams/search—q,sport,league,cursor,limit(1–50),sort(rank|name|fans)GET /api/teams/followedGET /api/me/starred-teams— authenticated HQ bookmarks (not follows)PUT /api/me/starred-teams/{teamId}— star a club (idempotent)DELETE /api/me/starred-teams/{teamId}— unstar a club (idempotent)POST /api/me/starred-teams— additive snapshot merge for the one-time client migrationPOST /api/teams/{id}/follow— social follow (TeamFollowonly)POST/DELETE /api/teams/{id}/join— leaderboard membership only (idempotent)GET /api/teams/{id}/roster— public real-player roster for the lineup builderGET /api/teams/{id}/head-to-head?opponentId=— public series vs an opponent (recordplus the five most recent decided meetings).opponentIdis required. Missing or equal to{id}returns400.GET /api/teams/{id}/achievements— public team badgesGET /api/user/teams—{ joinedTeamIds, followedTeamIds }
GET /api/teams/{id}, starred teams, and GET /api/user/teams are in the generated OpenAPI.
Sports catalog
GET /api/config/sportsis public. Auth is optional.- Payload is
{ sports: [{ id, label, order, leagues: [{ id, label, order }] }] }derived fromLeague.sport. - There is no second hardcoded sports list. Native may fall back to a local cache on
500. - Team payloads expose never-null
sportandleagueInfo.sport. Unknown or empty values are"other".
Leagues endpoints
GET /api/leaguesGET /api/leagues/{leagueId}/standings— public HQ standings.leagueIdis the PrismaLeague.idor slug (not a vendor NHL id). Response is{ rows, tiebreakRules, tables }.rowsis one team each (division, else conference, else league).tableslists every remaining scope.tiebreakRulesnames the fallback only.404if the league is missing. Emptyrowswhen the league cannot be scored honestly.
Sync-trigger endpoints
POST /api/admin/teams/sync(admin only, documented with redaction)POST /api/admin/leagues/sync(admin only, documented with redaction)POST /api/webhooks/leagues-sync(integration reference only; secret details redacted)
Join vs follow
POST /api/teams/{id}/followwritesTeamFollow. It does not create a leaderboard entry.POST /api/teams/{id}/joinupserts aLeaderboardEntryon the team board. It does not writeTeamFollow.DELETE /api/teams/{id}/joinremoves only the leaderboard entry. Social follow stays in place.- Both join and leave are idempotent and return the same
{ joinedTeamIds, followedTeamIds }shape asGET /api/user/teams.
Starred HQ teams
A star is a bookmark. It does not writeTeamFollow and it does not grant a social surface, so provider clubs that have no synced Team row are allowed.
GET /api/me/starred-teamsreturns{ teams }for the caller. Each entry is{ id, slug, name, logo, source, providerId, starredAt }.slug,name, andlogoresolve live fromTeamwhen a row exists and otherwise fall back to the stored client snapshot. All three can be null.sourceandproviderIdare null when noTeamrow exists.PUT /api/me/starred-teams/{teamId}is idempotent. Optional body{ slug?, name?, logo? }stores the display snapshot. Re-starring refreshes only the keys you send. An explicitnullclears a stored value. A new id past the 50-club cap returns409 STARRED_TEAMS_LIMIT.DELETE /api/me/starred-teams/{teamId}is idempotent. A missing star still returns{ starred: false }.POST /api/me/starred-teamsis an additive merge, not a replace. Body is{ teams: [{ id, slug?, name?, logo? }] }. Existing ids refresh their snapshot. New ids star until the cap. Entries past the cap are counted inskipped, with per-entry detail inrejected(reasonisinvalidorover_cap). The route returns{ teams, added, skipped, rejected }.
Client integration notes
/api/teamspayload includes enriched team context (ranking/count, league-link metadata, and never-nullsport).GET /api/teamsreturns{ teams, season, total }when you filter or paginate. The unfiltered full list omitstotal.?slug=returns{ team }. Archived-team slug checks are case-insensitive./api/leaguespayloads include branding/color metadata used in client visual theming.GET /api/teams/{id}/rosterreturns{ players: [{ id, name, jersey, position, role, duty, photo?, availability? }] }from activePlayerrows.photois omitted when there is no art.availabilityis omitted when the player is available. Editorial snapshots live inleagues/teams/rosters.json.- After sync events, clients should tolerate short propagation windows before all surfaces reflect updates.
- Users can favorite at most 5 teams. Official
d-sportsandhouse-of-dogedo not count toward that cap. HQ stars are a separate bookmark list with a 50-club cap.
