CBB Stats Developer Portal
Free, keyless JSON API for NCAA Division I basketball data: OpenAPI 3.1, markdown and CSV table formats, OAuth 2.0 for user data, versioned with a documented deprecation policy.
When to use this API
Use CBB Stats when a task needs current-season men's NCAA Division I player or team numbers with percentile context. It is the right tool for:
- Ranking or filtering players by a combination of stats ("top 20 sophomores by BPM and assist rate with at least 60% minutes").
- Answering "who is in the transfer portal" with BPR, transfer value and position scarcity.
- Projecting a team's or a conference's depth chart for next season.
- Comparing two players with national percentiles for each stat.
- Team strength via FEDERER ratings, including rank movement between daily snapshots and per-team history.
- Referee tendencies (spread deviation, foul splits).
When not to use it
It is the wrong tool for live scores, play-type data, jersey numbers, women's basketball or non-Division-I teams — none of those are in the data. Call it directly with HTTPS GET requests; no SDK is required.
Quickstart
No key, no account, no sign-up. Every read endpoint answers JSON by default, a markdown table for Accept: text/markdown or ?format=md, and CSV for ?format=csv.
curl "https://stats-cbb.com/api/v1/players?stats=BPM,PPG&format=md"
curl "https://stats-cbb.com/api/v1/players/search?q=edey"
curl -H "Accept: text/markdown" "https://stats-cbb.com/api/v1/rankings"
curl "https://stats-cbb.com/api/v1/rankings/diff?format=csv"Endpoints
The OpenAPI 3.1 document is the contract: every operation has an operationId, a description, typed parameters and typed response schemas, and it is generated from the running code so it cannot go stale.
- GET /api/v1/players?stats=… — rank players on 1–6 stats
- GET /api/v1/players/search?q= — name autocomplete
- GET /api/v1/players/{id} — one player, with percentiles and prior-season stats
- GET /api/v1/players/{id}/similar — nearest players in z-score space
- GET /api/v1/players/compare?p1=&p2= — head to head
- GET /api/v1/players/portal — transfer portal with BPR and transfer value
- GET /api/v1/players/depth-chart?conference= — projected depth charts
- GET /api/v1/rankings, /diff, /history, /snapshots — FEDERER team ratings
- GET /api/v1/referee — referee ratings
- GET /api/health — liveness and database status
- OpenAPI document
- llms.txt — short orientation file for language models
- llms-full.txt — llms.txt plus this page and the About page
Formats and content negotiation
- application/json (default).
- text/markdown; charset=utf-8 for Accept: text/markdown or ?format=md — a GitHub-flavoured pipe table. q-values are honoured; JSON wins a wildcard Accept.
- text/csv for ?format=csv (RFC 4180, served as a download).
- Every negotiable response carries Vary: Accept. An Accept header that lists none of the supported types gets 406 with the list.
- HTML pages negotiate too: send Accept: text/markdown to any page, or append .md to its path (/about.md, /player/{id}.md, /index.md).
Errors
Every error under /api is JSON, including 404s for unknown routes and malformed request bodies:
{ "error": "Invalid stat: XYZ", "code": "bad_request", "status": 400,
"hint": "Check the query parameters and body against the OpenAPI spec at https://stats-cbb.com/api/openapi.json.",
"docs": "https://stats-cbb.com/developers#errors" }- code is stable and machine-readable: bad_request, unauthorized, forbidden, not_found, conflict, rate_limited, server_error, service_unavailable.
- OAuth endpoints use the RFC 6749 shape instead: { "error": "invalid_grant", "error_description": "…" }.
Rate limits
300 requests per 15 minutes per IP across /api, with standard RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers; 20 per 15 minutes for authentication and OAuth token and registration endpoints. A 429 body carries code "rate_limited". Cached responses (search 5 min, player profiles 5 min, depth charts 10 min) do not change the limit.
Versioning and deprecation policy
- The path carries the version: /api/v1/… is the stable contract. Bare /api/… serves the identical surface and is not deprecated; it is what the web app uses.
- Every /api response carries an API-Version header (currently 1).
- Breaking changes ship as a new major path (/api/v2) with the previous major kept for at least six months.
- A deprecated endpoint announces itself in three places at once: 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 deprecated: true on the operation in the OpenAPI document.
- Additive changes (new fields, new optional parameters, new endpoints) are not breaking and can appear at any time; clients should ignore fields they do not know.
Authentication and OAuth 2.0
Read endpoints are anonymous. User-scoped endpoints (watchlist, posting comments) accept a Bearer JWT from either the site's own login (POST /api/auth/login) or an OAuth 2.0 authorization-code grant with PKCE, which is what a third-party app or an agent acting for a user should use.
- Authorization server metadata (RFC 8414): /.well-known/oauth-authorization-server
- Protected resource metadata (RFC 9728): /.well-known/oauth-protected-resource
- Register a client (RFC 7591, self-serve, no approval step): POST /api/oauth/register with {"client_name", "redirect_uris"}; you get a client_id back. Public clients only — there is no client secret; PKCE S256 is required.
- Authorization endpoint: GET /oauth/authorize?response_type=code&client_id=…&redirect_uri=…&scope=read%20watchlist&state=…&code_challenge=…&code_challenge_method=S256 — the user signs in and approves; the code returns to redirect_uri.
- Token endpoint: POST /api/oauth/token (application/x-www-form-urlencoded or JSON) with grant_type=authorization_code, code, redirect_uri, client_id, code_verifier → {"access_token", "token_type": "Bearer", "expires_in": 28800, "scope"}.
- Scopes: read (default; everything anonymous already allows) and watchlist (read and write the user's watchlist, post comments). Tokens expire after 8 hours; there are no refresh tokens yet — run the flow again.
Free tier, keys and sandbox
There is one tier and it is free. Read endpoints need no key at all; OAuth clients register themselves in one request. There is no separate sandbox because the production read API is read-only and idempotent — it is safe to develop against directly, and the rate limit above is the only quota. If you need higher limits for a research project, open an issue.
Command-line tool
The cbb-stats CLI wraps every read endpoint and prints JSON, CSV or a markdown table. It has no dependencies beyond Node 20.
npx cbb-stats-cli players --stats BPM,PPG --format md
npx cbb-stats-cli rankings diff