{"openapi":"3.1.0","info":{"title":"CBB Stats API","version":"1.0.0","summary":"NCAA Division I men's basketball player and team analytics.","description":"Read API behind stats-cbb.com, versioned under /api/v1 (bare /api is an identical alias). Covers per-player stats with national percentiles, transfer portal status with transfer value, projected depth charts, head-to-head comparison, FEDERER team ratings with daily snapshot history, and referee ratings. Read endpoints need no key. User-scoped endpoints (watchlist, comments) accept a bearer JWT from POST /api/v1/auth/login or from the OAuth 2.0 authorization-code + PKCE flow (see the oauth tag and /.well-known/oauth-authorization-server). Every error is JSON: { error, code, status, hint, docs }.","contact":{"name":"CBB Stats","url":"https://stats-cbb.com/contact"},"license":{"name":"ISC","url":"https://opensource.org/license/isc-license-txt"},"termsOfService":"https://stats-cbb.com/privacy","x-deprecation-policy":"The path carries the version: /api/v1/... is the stable contract; bare /api/... is the identical surface and is not deprecated. Every /api response carries an API-Version header. Breaking changes ship as a new major path (/api/v2) with the previous major kept for at least six months. A deprecated endpoint sends a Deprecation header (RFC 9745, @unix-timestamp), a Sunset header (RFC 8594, HTTP-date of removal) with a Link rel=\"deprecation\" to the migration note, and carries deprecated: true in this document. Additive changes (new fields, optional parameters, endpoints) are not breaking; ignore fields you do not know.","x-rate-limit":"300 requests / 15 minutes per IP across /api (RateLimit-* headers); 20 / 15 minutes on /auth and /oauth."},"externalDocs":{"url":"https://stats-cbb.com/developers","description":"Developer portal: quickstart, formats, errors, versioning, OAuth."},"servers":[{"url":"https://stats-cbb.com","description":"Production"}],"tags":[{"name":"players","description":"Per-player stats, rankings, portal, depth charts, comparison."},{"name":"rankings","description":"FEDERER team ratings and their daily snapshot history."},{"name":"referees","description":"Referee ratings from spread deviation."},{"name":"watchlist","description":"A signed-in user's saved players (bearer token or OAuth watchlist scope)."},{"name":"auth","description":"First-party accounts: register and sign in for a bearer JWT."},{"name":"oauth","description":"OAuth 2.0 authorization server: client registration, consent, token exchange."},{"name":"meta","description":"Service health."}],"paths":{"/api/v1/players":{"get":{"operationId":"listPlayers","tags":["players"],"summary":"Rank players by a combination of stats","description":"Returns players scored on the combined percentile of 1-6 stats, secondary-sorted by raw values. Every player in the season is rankable — departed players and tiny samples included; filterMin=true is the opt-in that drops players under 15% of minutes. Lower-is-better stats (TO, FC40, DRTG) are inverted so 100 is always good. Results are cached for five minutes.","parameters":[{"name":"stats","in":"query","required":true,"description":"Comma-separated stat keys, 1-6 of them, from the Stat schema. Duplicates are rejected.","schema":{"type":"string"},"example":"PPG,RPG,eFG"},{"name":"season","in":"query","required":false,"description":"Season as the year it ends (2026 = 2025-26). Defaults to the current season; anything but a four-digit year is rejected with 400.","schema":{"type":"integer","minimum":2000,"maximum":2100},"example":2026},{"name":"filterMin","in":"query","required":false,"description":"true to require at least 15% of minutes.","schema":{"type":"string","enum":["true"]}},{"name":"filters","in":"query","required":false,"description":"URL-encoded JSON array of {stat, type: 'min'|'max', value} thresholds.","schema":{"type":"string"},"example":"%5B%7B%22stat%22%3A%22Min%22%2C%22type%22%3A%22min%22%2C%22value%22%3A%2250%22%7D%5D"},{"name":"classes","in":"query","required":false,"description":"Comma-separated class years to keep, e.g. Fr,So.","schema":{"type":"string"}},{"name":"minHeight","in":"query","required":false,"description":"Minimum height in inches.","schema":{"type":"integer"}},{"name":"maxHeight","in":"query","required":false,"description":"Maximum height in inches.","schema":{"type":"integer"}},{"name":"positions","in":"query","required":false,"description":"Comma-separated position slots (PG, SG, SF, PF, C).","schema":{"type":"string"}},{"name":"portalOnly","in":"query","required":false,"description":"true to restrict to transfer-portal players.","schema":{"type":"string","enum":["true"]}},{"name":"hmFilter","in":"query","required":false,"description":"hm = power-conference teams only; non_hm = everyone else.","schema":{"type":"string","enum":["hm","non_hm"]}},{"name":"top100","in":"query","required":false,"description":"true to use Top-100-competition splits instead of full-season stats.","schema":{"type":"string","enum":["true"]}},{"name":"breakout","in":"query","required":false,"description":"true to keep only non-seniors with 15-45% minutes and a top-20% BPM or BPR (breakout candidates).","schema":{"type":"string","enum":["true"]}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Ranked players","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RankedPlayersResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"400":{"description":"Missing stats param, unknown stat key, duplicate stats, malformed filters, or an invalid season","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited: 300 requests / 15 min per IP across /api; 20 / 15 min on auth and OAuth endpoints. Wait for RateLimit-Reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/search":{"get":{"operationId":"searchPlayers","tags":["players"],"summary":"Name autocomplete","description":"Case-insensitive substring match on player name, treated as literal text rather than a pattern. Returns at most 10 results, so it is the way to turn a name into a player id.","parameters":[{"name":"q","in":"query","required":true,"description":"Search fragment, minimum 2 characters.","schema":{"type":"string","minLength":2},"example":"edey"}],"responses":{"200":{"description":"Matching players","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"Query missing or shorter than 2 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited: 300 requests / 15 min per IP across /api; 20 / 15 min on auth and OAuth endpoints. Wait for RateLimit-Reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/seasons":{"get":{"operationId":"listPlayerSeasons","tags":["players"],"summary":"Seasons with player data","description":"Which seasons the player store holds, newest first, plus the current one — the values the season query parameter on /players accepts.","responses":{"200":{"description":"Seasons, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Seasons"}}}}}}},"/api/v1/players/compare":{"get":{"operationId":"comparePlayers","tags":["players"],"summary":"Compare two players","description":"Both players with every stat and its national percentile against the pool of players with at least 15% of minutes. The comparison is also recorded for the leaderboard unless p1 equals p2.","parameters":[{"name":"p1","in":"query","required":true,"description":"First player id.","schema":{"type":"string"}},{"name":"p2","in":"query","required":true,"description":"Second player id.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Both players with percentile context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"400":{"description":"p1 or p2 missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Either player id not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/portal":{"get":{"operationId":"listPortalPlayers","tags":["players"],"summary":"Transfer portal, ranked by transfer value","description":"Every player in (or committed out of) the transfer portal with per-game averages, BPR/OBPR/DBPR, and TV — BPR scaled by class year and positional scarcity — sorted by TV descending. positionScarcity reports the market signal per slot.","parameters":[{"name":"positions","in":"query","required":false,"description":"Comma-separated position slots to keep (PG, SG, SF, PF, C).","schema":{"type":"string"}},{"name":"conference","in":"query","required":false,"description":"Restrict to players whose current team is in this conference, e.g. ACC or Big Ten.","schema":{"type":"string"}},{"name":"classes","in":"query","required":false,"description":"Comma-separated class years to keep.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Portal players, conferences, and position scarcity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/depth-chart":{"get":{"operationId":"getDepthChart","tags":["players"],"summary":"Projected depth charts for a conference","description":"Five position slots per team sorted by minute share. Forward-looking: seniors, departed and portal players are excluded and class years display one year ahead. teamProfile carries percentile bars for the team versus the national pool. Cached for ten minutes.","parameters":[{"name":"conference","in":"query","required":true,"description":"Conference name exactly as listed in the response's conferences array, e.g. ACC.","schema":{"type":"string"},"example":"ACC"},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Every team in the conference with its depth chart","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepthChartResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"400":{"description":"Missing or unknown conference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/leaderboard":{"get":{"operationId":"getComparisonLeaderboard","tags":["players"],"summary":"Head-to-head comparison leaderboard","description":"The 50 players who have won the most recorded comparisons, with wins, losses and ties. A comparison is recorded every time two different players are compared on the site, the bot or the API.","parameters":[{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Top 50 by comparison wins","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/{playerId}":{"get":{"operationId":"getPlayer","tags":["players"],"summary":"One player's full profile","description":"Every stored field: identity, season, the full stat map, Top-100-competition splits, prior-season stats, precomputed national percentiles, highlight video, whether the player is a breakout candidate, and otherSeasons — the same player's documents from other seasons (linked by name and school). Ids resolve across seasons; a prior-season document's id carries a -<season> suffix. Cached for five minutes.","parameters":[{"name":"playerId","in":"path","required":true,"description":"Player id from search or a ranking response.","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Player profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlayerProfile"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"404":{"description":"Player not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/{playerId}/similar":{"get":{"operationId":"getSimilarPlayers","tags":["players"],"summary":"Statistically similar players","description":"Nearest players by Euclidean distance in z-score space across 20 stat dimensions, drawn from players with at least 15% of minutes, each with a 0-100 similarity percent.","parameters":[{"name":"playerId","in":"path","required":true,"description":"Player id.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"How many neighbours to return, 1-25.","schema":{"type":"integer","minimum":1,"maximum":25,"default":3}}],"responses":{"200":{"description":"Nearest players","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimilarResponse"}}}},"404":{"description":"Player not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/{playerId}/growth-prediction":{"get":{"operationId":"getGrowthPrediction","tags":["players"],"summary":"Next-season stat projection","description":"Model-projected next-season stats from the ML service. Answers { available: false } at 200 when the player has no stats, is a senior, or the service is unavailable, so callers never break on it.","parameters":[{"name":"playerId","in":"path","required":true,"description":"Player id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Prediction, or available: false","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrowthPrediction"}}}},"404":{"description":"Player not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/players/{playerId}/comments":{"get":{"operationId":"listPlayerComments","tags":["players"],"summary":"Comments on a player","description":"The 100 most recent user comments on a player's profile, newest first. Public.","parameters":[{"name":"playerId","in":"path","required":true,"description":"Player id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Comments, newest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}}}}},"404":{"description":"Player not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addPlayerComment","tags":["players"],"summary":"Post a comment on a player","description":"Adds a comment (1-2000 characters) under the signed-in user's name. Requires a bearer token; an OAuth token needs the watchlist scope.","security":[{"oauth2":["watchlist"]},{"bearer":[]}],"parameters":[{"name":"playerId","in":"path","required":true,"description":"Player id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewComment"}}}},"responses":{"201":{"description":"The stored comment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"}}}},"400":{"description":"Empty or over-long comment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid bearer token. The WWW-Authenticate header names the OAuth metadata URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The token is valid but lacks the watchlist scope (OAuth tokens only).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"404":{"description":"Player or user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/rankings":{"get":{"operationId":"getRankings","tags":["rankings"],"summary":"FEDERER team ratings","description":"Possession-exchange team ratings for every Division I team with at least 25 processed games, ranked on the chosen sort. Non-Division-I teams are filtered from every response.","parameters":[{"name":"season","in":"query","required":false,"description":"Season year (the year the season ends). Defaults to the current season.","schema":{"type":"integer"},"example":2026},{"name":"sort","in":"query","required":false,"description":"Metric to rank on.","schema":{"type":"string","enum":["federerPct","federerPctExclTies","federerNet","trueFedererPct","exchangesWon","sosAdjustedElo","federerElo"],"default":"federerPct"}},{"name":"order","in":"query","required":false,"description":"asc or desc (default).","schema":{"type":"string","enum":["asc","desc"]}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Ranked teams","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RankingsResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/rankings/diff":{"get":{"operationId":"getRankingsDiff","tags":["rankings"],"summary":"Rank movement between two daily snapshots","description":"Only the teams whose rank changed between the from and to snapshots (default: the latest two), with a delta column — positive means the team climbed, null means it entered or exited. Snapshots are captured daily; until two exist the endpoint answers 200 with an empty changes list and a note.","parameters":[{"name":"from","in":"query","required":false,"description":"Snapshot date YYYY-MM-DD. Default: the snapshot preceding to.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Snapshot date YYYY-MM-DD. Default: the latest snapshot.","schema":{"type":"string","format":"date"}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Changed rows only, ordered by new rank","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RankingsDiffResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"404":{"description":"No snapshot exists for an explicitly named date","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/rankings/snapshots":{"get":{"operationId":"listRankingSnapshots","tags":["rankings"],"summary":"Which ranking-snapshot dates exist","description":"The availability index for diff and history: date, season and team count per stored snapshot, newest first, without the team arrays. Call this to learn which windows a diff or history query can address.","parameters":[{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Snapshot index, newest first; empty plus a note while none exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotsResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/rankings/history":{"get":{"operationId":"getRankingHistory","tags":["rankings"],"summary":"One team's daily rank series","description":"Walks the daily ranking snapshots and returns a time series of the named team's rank and metric value — the way to ask whether a team has been consistently rising or had a one-week blip. Days the team was unranked have no point.","parameters":[{"name":"team","in":"query","required":true,"description":"Canonical team name, case-insensitive, e.g. Duke.","schema":{"type":"string"},"example":"Duke"},{"name":"days","in":"query","required":false,"description":"Window size in days, ending today. Non-numeric values are rejected with 400.","schema":{"type":"integer","minimum":1,"maximum":365,"default":30}},{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"date/rank/value points in snapshot order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"400":{"description":"Missing team param or non-numeric days","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Team appears in no snapshot in the window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/rankings/health":{"get":{"operationId":"getRankingsHealth","tags":["rankings"],"summary":"Why the rankings table may be incomplete","description":"Which team records are excluded from the rankings and why: an ESPN name that does not resolve to a canonical Division I team, or too few processed games. Operational, but public.","parameters":[{"name":"season","in":"query","required":false,"description":"Season year. Defaults to the current season.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Exclusion report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RankingsHealth"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/rankings/seasons":{"get":{"operationId":"listRankingSeasons","tags":["rankings"],"summary":"Seasons with team ratings","description":"Which seasons have FEDERER team data, newest first, plus the current one — the values the season query parameter on /rankings accepts. Kept separate from /players/seasons because the two syncs fill different seasons.","responses":{"200":{"description":"Seasons, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Seasons"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/referee":{"get":{"operationId":"listReferees","tags":["referees"],"summary":"Referee ratings","description":"Referees with at least 5 officiated games, sorted by refScore descending. refScore is 0.5 divided by average deviation from the closing spread, so higher is better; foul splits by half and by home/away are included.","parameters":[{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Ranked referees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefereesResponse"}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/watchlist/trending":{"get":{"operationId":"getTrendingPlayers","tags":["watchlist"],"summary":"Most-viewed players right now","description":"The three players with the highest trending score (views, decayed nightly). Public; cached for two minutes.","responses":{"200":{"description":"Up to three players","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TrendingPlayer"}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/watchlist":{"get":{"operationId":"getWatchlist","tags":["watchlist"],"summary":"The signed-in user's watchlist","description":"Every saved player with the stats it was saved for, their current values, and the full stat map. Requires a bearer token; an OAuth token needs the watchlist scope.","security":[{"oauth2":["watchlist"]},{"bearer":[]}],"parameters":[{"name":"format","in":"query","required":false,"description":"Table export: json and csv as downloads, md as an inline markdown table (also served for Accept: text/markdown).","schema":{"type":"string","enum":["json","csv","md"]}}],"responses":{"200":{"description":"Watchlist entries","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/WatchlistEntry"}}},"text/markdown":{"schema":{"type":"string","description":"GitHub-flavoured pipe table (Accept: text/markdown or ?format=md)."}},"text/csv":{"schema":{"type":"string","description":"RFC 4180 CSV, served as a download (?format=csv)."}}}},"401":{"description":"Missing or invalid bearer token. The WWW-Authenticate header names the OAuth metadata URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The token is valid but lacks the watchlist scope (OAuth tokens only).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"addWatchlistEntry","tags":["watchlist"],"summary":"Save a player to the watchlist","description":"Adds a player with the stat keys to track. The same player may be saved more than once with different stat sets; the same set twice is a conflict.","security":[{"oauth2":["watchlist"]},{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchlistRequest"}}}},"responses":{"201":{"description":"Added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"playerId or stats missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid bearer token. The WWW-Authenticate header names the OAuth metadata URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The token is valid but lacks the watchlist scope (OAuth tokens only).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"404":{"description":"Player or user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already saved with these stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/watchlist/{playerId}":{"delete":{"operationId":"removeWatchlistEntry","tags":["watchlist"],"summary":"Remove a watchlist entry","description":"Removes the entry for this player whose stat set matches the stats query param exactly (order-insensitive).","security":[{"oauth2":["watchlist"]},{"bearer":[]}],"parameters":[{"name":"playerId","in":"path","required":true,"description":"Player id.","schema":{"type":"string"}},{"name":"stats","in":"query","required":false,"description":"Comma-separated stat keys the entry was saved with.","schema":{"type":"string"},"example":"PPG,RPG"}],"responses":{"200":{"description":"Removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"description":"Missing or invalid bearer token. The WWW-Authenticate header names the OAuth metadata URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The token is valid but lacks the watchlist scope (OAuth tokens only).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"404":{"description":"Entry or user not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/register":{"post":{"operationId":"registerAccount","tags":["auth"],"summary":"Create a first-party account","description":"Creates a username/password account and returns an 8-hour bearer JWT. Third-party apps should use the OAuth flow instead of collecting passwords.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credentials"}}}},"responses":{"201":{"description":"Account created; token issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthToken"}}}},"400":{"description":"Username under 3 or password under 6 characters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Username already taken","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited: 300 requests / 15 min per IP across /api; 20 / 15 min on auth and OAuth endpoints. Wait for RateLimit-Reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/login":{"post":{"operationId":"login","tags":["auth"],"summary":"Sign in for a bearer token","description":"Exchanges a username and password for an 8-hour bearer JWT accepted by every user-scoped endpoint. Discord-only accounts must sign in through the site.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credentials"}}}},"responses":{"200":{"description":"Token issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthToken"}}}},"400":{"description":"Username or password missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited: 300 requests / 15 min per IP across /api; 20 / 15 min on auth and OAuth endpoints. Wait for RateLimit-Reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/oauth/register":{"post":{"operationId":"registerOAuthClient","tags":["oauth"],"summary":"Register an OAuth client (RFC 7591)","description":"Self-serve registration of a public client: no approval step, no client secret. Redirect URIs must be https, or http on localhost/127.0.0.1, with no fragment. PKCE S256 is required at authorization time.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClientRegistration"}}}},"responses":{"201":{"description":"Registered client, with its client_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClient"}}}},"400":{"description":"invalid_redirect_uri or invalid_client_metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited: 300 requests / 15 min per IP across /api; 20 / 15 min on auth and OAuth endpoints. Wait for RateLimit-Reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"server_error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/v1/oauth/client/{clientId}":{"get":{"operationId":"getOAuthClient","tags":["oauth"],"summary":"Public details of a registered client","description":"What the consent page shows the user before they approve: name, homepage, registered redirect URIs and the scopes the client may request.","parameters":[{"name":"clientId","in":"path","required":true,"description":"The client_id from registration.","schema":{"type":"string"}}],"responses":{"200":{"description":"Client details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthClientPublic"}}}},"404":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"500":{"description":"server_error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/v1/oauth/authorize":{"post":{"operationId":"createAuthorizationCode","tags":["oauth"],"summary":"Approve an authorization request (consent step)","description":"Called by the /oauth/authorize page with the signed-in user's own bearer token after they approve. Validates the client, redirect_uri, scope and PKCE challenge, mints a single-use five-minute code, and returns the exact redirect to follow. Never redirects itself.","security":[{"bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizeRequest"}}}},"responses":{"200":{"description":"Code issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthorizeResponse"}}}},"400":{"description":"invalid_request, invalid_client or invalid_scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"The user is not signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"server_error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/v1/oauth/token":{"post":{"operationId":"exchangeAuthorizationCode","tags":["oauth"],"summary":"Exchange a code for an access token (RFC 6749 §4.1.3)","description":"Trades an authorization code plus its PKCE code_verifier for an 8-hour bearer JWT carrying the user and the granted scope. Codes are single use. The response is marked Cache-Control: no-store.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}},"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"invalid_request, invalid_grant or unsupported_grant_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited: 300 requests / 15 min per IP across /api; 20 / 15 min on auth and OAuth endpoints. Wait for RateLimit-Reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/health":{"get":{"operationId":"getHealth","tags":["meta"],"summary":"Liveness and database status","description":"200 when the process is up and its MongoDB connection is open, 503 otherwise. Not rate limited, so pollers cannot make it flap.","responses":{"200":{"description":"Healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"503":{"description":"Database disconnected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}}}}}},"components":{"securitySchemes":{"oauth2":{"type":"oauth2","description":"Authorization code with PKCE (S256) for public clients. Register at POST /api/v1/oauth/register; metadata at /.well-known/oauth-authorization-server.","flows":{"authorizationCode":{"authorizationUrl":"https://stats-cbb.com/oauth/authorize","tokenUrl":"https://stats-cbb.com/api/oauth/token","scopes":{"read":"Read public stats (everything anonymous access already allows).","watchlist":"Read and change the user's watchlist and post comments as them."}}}},"bearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Error":{"type":"object","description":"Every non-OAuth error under /api.","properties":{"error":{"type":"string","description":"Human-readable message."},"code":{"type":"string","description":"Stable machine-readable code: bad_request, unauthorized, forbidden, not_found, conflict, rate_limited, server_error, service_unavailable, ..."},"status":{"type":"integer","description":"The HTTP status, repeated for clients that lose it."},"hint":{"type":"string","description":"What to do about it."},"docs":{"type":"string","format":"uri","description":"Where the error format is documented."}},"required":["error","code","status"]},"OAuthError":{"type":"object","description":"RFC 6749 §5.2 error body, used by the oauth endpoints and by 403 insufficient_scope.","properties":{"error":{"type":"string","description":"invalid_request, invalid_client, invalid_grant, invalid_scope, unsupported_grant_type, invalid_redirect_uri, invalid_client_metadata, insufficient_scope, server_error."},"error_description":{"type":"string"}},"required":["error"]},"Stat":{"type":"string","description":"A stat key accepted by the stats query param and used in every stat map.","enum":["PPG","RPG","APG","eFG","TS","OR","DR","ARate","TO","Blk","Stl","FTRate","FT","2P","3P","Min","G","ORTG","DRTG","Usg","FTA","FTM","2PM","2PA","3PM","3PA","FC40","Close2PM","Close2PA","Close2P","Far2PM","Far2PA","Far2P","DunksAtt","DunksMade","DunkPct","BPM","OBPM","DBPM","3P100","fedPossessions","OBPR","DBPR","BPR"]},"StatMap":{"type":"object","description":"Stat key (see the Stat schema) to numeric value.","additionalProperties":{"type":"number"}},"PlayerSummary":{"type":"object","properties":{"id":{"type":"string","description":"Stable player id; the value used in /player/{id} and every playerId parameter. Prior-season documents carry a -<season> suffix."},"season":{"type":"integer","description":"Season as the year it ends (2026 = 2025-26)."},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string","description":"Stored class year (Fr/So/Jr/Sr). Depth chart views display this +1."},"position":{"type":"string","description":"Torvik position label, e.g. Combo G, Wing F, PF/C."},"height":{"type":"string","description":"Feet-inches, e.g. 6-7."},"inPortal":{"type":"boolean"},"imageUrl":{"type":"string","description":"Headshot URL. Absent when no photo exists."}}},"RankedPlayer":{"type":"object","properties":{"id":{"type":"string","description":"Stable player id; the value used in /player/{id} and every playerId parameter. Prior-season documents carry a -<season> suffix."},"season":{"type":"integer","description":"Season as the year it ends (2026 = 2025-26)."},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string","description":"Stored class year (Fr/So/Jr/Sr). Depth chart views display this +1."},"position":{"type":"string","description":"Torvik position label, e.g. Combo G, Wing F, PF/C."},"height":{"type":"string","description":"Feet-inches, e.g. 6-7."},"inPortal":{"type":"boolean"},"imageUrl":{"type":"string","description":"Headshot URL. Absent when no photo exists."},"statValues":{"type":"object","description":"Raw value of each requested stat.","additionalProperties":{"type":"number"}},"statPcts":{"type":"object","description":"Percentile (0-100, inverted for lower-is-better stats) of each requested stat.","additionalProperties":{"type":"number"}},"combined":{"type":"number","description":"Sum of the requested percentiles; the ranking key."},"isBreakout":{"type":"boolean","description":"Only present with breakout=true."}}},"RankedPlayersResponse":{"type":"object","properties":{"statList":{"type":"array","items":{"$ref":"#/components/schemas/Stat"}},"season":{"type":"integer","description":"The season the results were ranked within."},"results":{"type":"array","items":{"$ref":"#/components/schemas/RankedPlayer"}}},"required":["statList","season","results"]},"SearchResponse":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/PlayerSummary"}}},"required":["results"]},"ComparePlayer":{"type":"object","properties":{"id":{"type":"string","description":"Stable player id; the value used in /player/{id} and every playerId parameter. Prior-season documents carry a -<season> suffix."},"season":{"type":"integer","description":"Season as the year it ends (2026 = 2025-26)."},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string","description":"Stored class year (Fr/So/Jr/Sr). Depth chart views display this +1."},"position":{"type":"string","description":"Torvik position label, e.g. Combo G, Wing F, PF/C."},"height":{"type":"string","description":"Feet-inches, e.g. 6-7."},"inPortal":{"type":"boolean"},"imageUrl":{"type":"string","description":"Headshot URL. Absent when no photo exists."},"stats":{"type":"object","description":"Full season stat map.","additionalProperties":{"type":"number"}},"statPcts":{"type":"object","description":"National percentile of every stat present.","additionalProperties":{"type":"number"}}}},"CompareResponse":{"type":"object","properties":{"playerA":{"$ref":"#/components/schemas/ComparePlayer"},"playerB":{"$ref":"#/components/schemas/ComparePlayer"}},"required":["playerA","playerB"]},"Highlight":{"type":"object","description":"Best-effort YouTube highlight reel. videoId null means the search ran and found nothing.","properties":{"videoId":{"type":["string","null"]},"title":{"type":"string"},"channel":{"type":"string"},"source":{"type":"string","enum":["official","fan","highschool","none"]},"matchReason":{"type":"string"},"confidence":{"type":"number"},"checkedAt":{"type":"string","format":"date-time"}}},"PlayerProfile":{"type":"object","properties":{"id":{"type":"string","description":"Stable player id; the value used in /player/{id} and every playerId parameter. Prior-season documents carry a -<season> suffix."},"season":{"type":"integer","description":"Season as the year it ends (2026 = 2025-26)."},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string","description":"Stored class year (Fr/So/Jr/Sr). Depth chart views display this +1."},"position":{"type":"string","description":"Torvik position label, e.g. Combo G, Wing F, PF/C."},"height":{"type":"string","description":"Feet-inches, e.g. 6-7."},"inPortal":{"type":"boolean"},"imageUrl":{"type":"string","description":"Headshot URL. Absent when no photo exists."},"heightInches":{"type":"number"},"weight":{"type":"number","description":"Pounds."},"depthRank":{"type":"integer","description":"1-indexed depth-chart tier within the player's position, when sourced."},"portalCommitted":{"type":"boolean"},"departed":{"type":"boolean","description":"Left college (e.g. NBA draft); excluded from rankings and depth charts."},"stats":{"type":"object","description":"Full season stat map.","additionalProperties":{"type":"number"}},"statsTop100":{"type":"object","description":"Splits against Top-100 competition.","additionalProperties":{"type":"number"}},"prevStats":{"type":"object","description":"Prior season stat map.","additionalProperties":{"type":"number"}},"precomputedPcts":{"type":"object","description":"National percentile of every stat present.","additionalProperties":{"type":"number"}},"highlight":{"$ref":"#/components/schemas/Highlight"},"isBreakout":{"type":"boolean"},"otherSeasons":{"type":"array","description":"The same player's documents in other seasons, newest first (linked by normalised name and school, so seasons at another program do not link).","items":{"type":"object","properties":{"season":{"type":"integer"},"id":{"type":"string"},"team":{"type":"string"},"year":{"type":"string"},"G":{"type":["number","null"]},"Min":{"type":["number","null"]},"BPM":{"type":["number","null"]},"BPR":{"type":["number","null"]}}}}}},"SimilarPlayer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string"},"position":{"type":"string"},"height":{"type":"string"},"inPortal":{"type":"boolean"},"similarityPercent":{"type":"number","description":"0-100; 100 is identical."}}},"SimilarResponse":{"type":"object","properties":{"similar":{"type":"array","items":{"$ref":"#/components/schemas/SimilarPlayer"}},"metric":{"type":"string","description":"Always euclidean_z."},"dimensions":{"type":"integer"},"poolFilter":{"type":"string"},"poolNote":{"type":"string"}},"required":["similar","metric","dimensions"]},"GrowthPrediction":{"type":"object","description":"When available is true the remaining members are the ML service's projected stats.","properties":{"available":{"type":"boolean"}},"additionalProperties":true},"Comment":{"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"body":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","username","body","createdAt"]},"NewComment":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":2000}},"required":["body"]},"PortalPlayer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"position":{"type":"string"},"team":{"type":"string","description":"The team the player is leaving."},"year":{"type":"string"},"height":{"type":"string"},"portalCommitted":{"type":"boolean"},"PPG":{"type":["number","null"]},"RPG":{"type":["number","null"]},"APG":{"type":["number","null"]},"BPR":{"type":["number","null"]},"OBPR":{"type":["number","null"]},"DBPR":{"type":["number","null"]},"TV":{"type":"number","description":"Transfer value: BPR scaled by class year and position scarcity."},"tvTier":{"type":"string"}}},"PositionScarcity":{"type":"object","description":"Keyed by slot (PG, SG, SF, PF, C).","additionalProperties":{"type":"object","properties":{"count":{"type":"integer","description":"Portal players at this slot."},"S":{"type":"number","description":"Scarcity factor, (min count / count) ^ 0.5."},"multiplier":{"type":"number","description":"1 + 0.3 × S; applied to TV."}}}},"PortalResponse":{"type":"object","properties":{"players":{"type":"array","items":{"$ref":"#/components/schemas/PortalPlayer"}},"conferences":{"type":"array","items":{"type":"string"}},"positionScarcity":{"$ref":"#/components/schemas/PositionScarcity"}},"required":["players","conferences","positionScarcity"]},"DepthChartPlayer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"inPortal":{"type":"boolean"},"year":{"type":"string","description":"Display class year: stored year + 1."},"height":{"type":"string"},"position":{"type":"string"},"imageUrl":{"type":"string"}},"additionalProperties":true},"DepthChartTeam":{"type":"object","properties":{"name":{"type":"string"},"depth":{"type":"object","description":"Players per slot, best first.","properties":{"PG":{"type":"array","items":{"$ref":"#/components/schemas/DepthChartPlayer"}},"SG":{"type":"array","items":{"$ref":"#/components/schemas/DepthChartPlayer"}},"SF":{"type":"array","items":{"$ref":"#/components/schemas/DepthChartPlayer"}},"PF":{"type":"array","items":{"$ref":"#/components/schemas/DepthChartPlayer"}},"C":{"type":"array","items":{"$ref":"#/components/schemas/DepthChartPlayer"}}}},"teamProfile":{"type":"object","properties":{"bars":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"portalPlayers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"height":{"type":["string","null"]},"position":{"type":["string","null"]},"year":{"type":["string","null"]}}}}}},"DepthChartResponse":{"type":"object","properties":{"conference":{"type":"string"},"conferences":{"type":"array","items":{"type":"string"}},"teams":{"type":"array","items":{"$ref":"#/components/schemas/DepthChartTeam"}}},"required":["conference","conferences","teams"]},"LeaderboardEntry":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string"},"position":{"type":"string"},"wins":{"type":"integer"},"losses":{"type":"integer"},"ties":{"type":"integer"},"total":{"type":"integer"}}},"LeaderboardResponse":{"type":"object","properties":{"leaderboard":{"type":"array","items":{"$ref":"#/components/schemas/LeaderboardEntry"}}},"required":["leaderboard"]},"Team":{"type":"object","properties":{"rank":{"type":"integer"},"espnTeamId":{"type":"string"},"teamName":{"type":"string","description":"Name as stored (ESPN form)."},"displayName":{"type":"string","description":"Canonical Division I name."},"conference":{"type":"string"},"exchangesWon":{"type":"integer"},"exchangesLost":{"type":"integer"},"exchangesTied":{"type":"integer"},"federerPct":{"type":["number","null"]},"federerPctExclTies":{"type":["number","null"]},"federerNet":{"type":["number","null"]},"trueWins":{"type":"integer"},"trueLosses":{"type":"integer"},"trueFedererPct":{"type":["number","null"]},"federerElo":{"type":["number","null"]},"sosAdjustedElo":{"type":["number","null"]},"gamesProcessed":{"type":"integer"},"gameWins":{"type":"integer"},"gameLosses":{"type":"integer"}}},"RankingsResponse":{"type":"object","properties":{"season":{"type":"integer"},"teams":{"type":"array","items":{"$ref":"#/components/schemas/Team"}}},"required":["season","teams"]},"RankChange":{"type":"object","properties":{"team":{"type":"string"},"fromRank":{"type":["integer","null"]},"toRank":{"type":["integer","null"]},"delta":{"type":["integer","null"],"description":"fromRank − toRank: positive = climbed, null = entered or exited."},"fromValue":{"type":["number","null"]},"toValue":{"type":["number","null"]}}},"RankingsDiffResponse":{"type":"object","properties":{"from":{"type":["string","null"],"format":"date"},"to":{"type":["string","null"],"format":"date"},"sort":{"type":["string","null"]},"changes":{"type":"array","items":{"$ref":"#/components/schemas/RankChange"}},"note":{"type":"string","description":"Present while fewer than two snapshots exist."}},"required":["changes"]},"Snapshot":{"type":"object","properties":{"date":{"type":"string","format":"date"},"season":{"type":"integer"},"teamCount":{"type":"integer"}}},"SnapshotsResponse":{"type":"object","properties":{"count":{"type":"integer"},"snapshots":{"type":"array","items":{"$ref":"#/components/schemas/Snapshot"}},"note":{"type":"string"}},"required":["count","snapshots"]},"HistoryPoint":{"type":"object","properties":{"date":{"type":"string","format":"date"},"rank":{"type":"integer"},"value":{"type":"number"}}},"HistoryResponse":{"type":"object","properties":{"team":{"type":"string"},"days":{"type":"integer"},"points":{"type":"array","items":{"$ref":"#/components/schemas/HistoryPoint"}},"note":{"type":"string"}},"required":["team","days","points"]},"RankingsHealth":{"type":"object","properties":{"season":{"type":"integer"},"minGamesProcessed":{"type":"integer"},"totals":{"type":"object","properties":{"teamStats":{"type":"integer"},"ranked":{"type":"integer"},"unresolvedName":{"type":"integer"},"belowGameThreshold":{"type":"integer"}}},"unresolvedName":{"type":"array","items":{"type":"object","additionalProperties":true}},"belowGameThreshold":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"Referee":{"type":"object","properties":{"espnId":{"type":"string"},"name":{"type":"string"},"totalReffed":{"type":"integer"},"spreadDifference":{"type":"number","description":"Average deviation from the closing spread."},"refScore":{"type":"number","description":"0.5 / spreadDifference; higher is better."},"halfFoulDiff":{"type":"number"},"homeFoulDiff":{"type":"number"},"crewChiefCount":{"type":"integer"}}},"RefereesResponse":{"type":"object","properties":{"referees":{"type":"array","items":{"$ref":"#/components/schemas/Referee"}}},"required":["referees"]},"Seasons":{"type":"object","properties":{"current":{"type":"integer","description":"The season every query defaults to."},"seasons":{"type":"array","items":{"type":"integer"},"description":"Newest first; always includes current."}},"required":["current","seasons"]},"Health":{"type":"object","properties":{"status":{"type":"string","enum":["ok","error"]},"db":{"type":"string","enum":["connected","disconnected"]},"uptime":{"type":"number","description":"Seconds."}},"required":["status","db","uptime"]},"TrendingPlayer":{"type":"object","properties":{"playerId":{"type":"string"},"name":{"type":"string"},"team":{"type":"string"}}},"WatchlistEntry":{"type":"object","properties":{"playerId":{"type":"string"},"stats":{"type":"array","items":{"$ref":"#/components/schemas/Stat"},"description":"The stat keys the entry was saved with."},"statValues":{"type":"object","description":"Current value of each saved stat.","additionalProperties":{"type":"number"}},"addedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"team":{"type":"string"},"year":{"type":"string"},"position":{"type":"string"},"allStats":{"type":"object","description":"The player's full stat map.","additionalProperties":{"type":"number"}}}},"WatchlistRequest":{"type":"object","properties":{"playerId":{"type":"string"},"stats":{"type":"array","items":{"$ref":"#/components/schemas/Stat"},"minItems":1}},"required":["playerId","stats"]},"Message":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]},"Credentials":{"type":"object","properties":{"username":{"type":"string","minLength":3},"password":{"type":"string","minLength":6}},"required":["username","password"]},"AuthToken":{"type":"object","properties":{"token":{"type":"string","description":"Bearer JWT, valid for 8 hours."},"username":{"type":"string"}},"required":["token","username"]},"OAuthClientRegistration":{"type":"object","description":"RFC 7591 client metadata. Only the public authorization-code profile is accepted.","properties":{"client_name":{"type":"string","minLength":1,"maxLength":100},"redirect_uris":{"type":"array","minItems":1,"items":{"type":"string","format":"uri"},"description":"https, or http on localhost / 127.0.0.1; no fragment."},"client_uri":{"type":"string","format":"uri","description":"The client's homepage (https)."},"scope":{"type":"string","description":"Space-separated subset of: read watchlist. Default read."},"grant_types":{"type":"array","items":{"type":"string","enum":["authorization_code"]}},"response_types":{"type":"array","items":{"type":"string","enum":["code"]}},"token_endpoint_auth_method":{"type":"string","enum":["none"]}},"required":["client_name","redirect_uris"]},"OAuthClient":{"type":"object","properties":{"client_id":{"type":"string"},"client_id_issued_at":{"type":"integer","description":"Unix seconds."},"client_name":{"type":"string"},"client_uri":{"type":"string","format":"uri"},"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"}},"scope":{"type":"string"},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_method":{"type":"string"}},"required":["client_id","client_id_issued_at","client_name","redirect_uris","scope"]},"OAuthClientPublic":{"type":"object","properties":{"client_id":{"type":"string"},"client_name":{"type":"string"},"client_uri":{"type":"string","format":"uri"},"redirect_uris":{"type":"array","items":{"type":"string","format":"uri"}},"scope":{"type":"string"}},"required":["client_id","client_name","redirect_uris","scope"]},"AuthorizeRequest":{"type":"object","properties":{"client_id":{"type":"string"},"redirect_uri":{"type":"string","format":"uri","description":"Must exactly match a registered redirect URI."},"scope":{"type":"string","description":"Space-separated; defaults to everything the client registered for."},"state":{"type":"string"},"code_challenge":{"type":"string","description":"BASE64URL(SHA256(code_verifier)), 43 characters."},"code_challenge_method":{"type":"string","enum":["S256"]}},"required":["client_id","redirect_uri","code_challenge","code_challenge_method"]},"AuthorizeResponse":{"type":"object","properties":{"code":{"type":"string","description":"Single use, expires in five minutes."},"state":{"type":"string"},"scope":{"type":"string"},"redirect_to":{"type":"string","format":"uri","description":"redirect_uri with code (and state) appended; the browser should navigate here."}},"required":["code","scope","redirect_to"]},"TokenRequest":{"type":"object","properties":{"grant_type":{"type":"string","enum":["authorization_code"]},"code":{"type":"string"},"redirect_uri":{"type":"string","format":"uri"},"client_id":{"type":"string"},"code_verifier":{"type":"string","minLength":43,"maxLength":128}},"required":["grant_type","code","redirect_uri","client_id","code_verifier"]},"TokenResponse":{"type":"object","properties":{"access_token":{"type":"string","description":"Bearer JWT."},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer","description":"Seconds; 28800."},"scope":{"type":"string"}},"required":["access_token","token_type","expires_in","scope"]}}}}