Skip to main content

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 team
  • GET /api/teams/{id} — public fetch by id or slug (id is tried first). Same team shape as a list entry, plus teamRecord, foundedDate, and viewerCanPostAsTeam. Archived teams return 404.
  • GET /api/teams/searchq, sport, league, cursor, limit (1–50), sort (rank | name | fans)
  • GET /api/teams/followed
  • GET /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 migration
  • POST /api/teams/{id}/follow — social follow (TeamFollow only)
  • POST/DELETE /api/teams/{id}/join — leaderboard membership only (idempotent)
  • GET /api/teams/{id}/roster — public real-player roster for the lineup builder
  • GET /api/teams/{id}/head-to-head?opponentId= — public series vs an opponent (record plus the five most recent decided meetings). opponentId is required. Missing or equal to {id} returns 400.
  • GET /api/teams/{id}/achievements — public team badges
  • GET /api/user/teams{ joinedTeamIds, followedTeamIds }
Search, join, roster, head-to-head, GET /api/teams/{id}, starred teams, and GET /api/user/teams are in the generated OpenAPI.

Sports catalog

  • GET /api/config/sports is public. Auth is optional.
  • Payload is { sports: [{ id, label, order, leagues: [{ id, label, order }] }] } derived from League.sport.
  • There is no second hardcoded sports list. Native may fall back to a local cache on 500.
  • Team payloads expose never-null sport and leagueInfo.sport. Unknown or empty values are "other".
This catalog route is in the generated OpenAPI.

Leagues endpoints

  • GET /api/leagues
  • GET /api/leagues/{leagueId}/standings — public HQ standings. leagueId is the Prisma League.id or slug (not a vendor NHL id). Response is { rows, tiebreakRules, tables }. rows is one team each (division, else conference, else league). tables lists every remaining scope. tiebreakRules names the fallback only. 404 if the league is missing. Empty rows when 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}/follow writes TeamFollow. It does not create a leaderboard entry.
  • POST /api/teams/{id}/join upserts a LeaderboardEntry on the team board. It does not write TeamFollow.
  • DELETE /api/teams/{id}/join removes only the leaderboard entry. Social follow stays in place.
  • Both join and leave are idempotent and return the same { joinedTeamIds, followedTeamIds } shape as GET /api/user/teams.

Starred HQ teams

A star is a bookmark. It does not write TeamFollow and it does not grant a social surface, so provider clubs that have no synced Team row are allowed.
  • GET /api/me/starred-teams returns { teams } for the caller. Each entry is { id, slug, name, logo, source, providerId, starredAt }. slug, name, and logo resolve live from Team when a row exists and otherwise fall back to the stored client snapshot. All three can be null. source and providerId are null when no Team row 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 explicit null clears a stored value. A new id past the 50-club cap returns 409 STARRED_TEAMS_LIMIT.
  • DELETE /api/me/starred-teams/{teamId} is idempotent. A missing star still returns { starred: false }.
  • POST /api/me/starred-teams is 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 in skipped, with per-entry detail in rejected (reason is invalid or over_cap). The route returns { teams, added, skipped, rejected }.

Client integration notes

  • /api/teams payload includes enriched team context (ranking/count, league-link metadata, and never-null sport).
  • GET /api/teams returns { teams, season, total } when you filter or paginate. The unfiltered full list omits total. ?slug= returns { team }. Archived-team slug checks are case-insensitive.
  • /api/leagues payloads include branding/color metadata used in client visual theming.
  • GET /api/teams/{id}/roster returns { players: [{ id, name, jersey, position, role, duty, photo?, availability? }] } from active Player rows. photo is omitted when there is no art. availability is omitted when the player is available. Editorial snapshots live in leagues/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-sports and house-of-doge do not count toward that cap. HQ stars are a separate bookmark list with a 50-club cap.