Every season of the CBA data site, newest first (the upcoming season is the default), 1995-96 on.
Endpoint: GET https://data-server.cbaleague.com/api/com-code-tables/getSeason
Value
A hoopR_data tibble: One row per season: label, START year, default flag. An empty valid answer is a zero-row
tibble with these columns, in this order (sportsdataverse-py cba_seasons):
| col_name | type | description |
| is_default | integer | Default-selection flag: 1 = the site's default season (exactly one row: the season in progress or next), 2 = not the default. |
| key | character | Season label 'YYYY-YYYY' (START year - END year), e.g. '2025-2026'; the sdv season key is the second year. The list runs from the upcoming season (2026-2027, 32 rows) down to 1995-1996 with no gap. |
| value | character | Season START year (2025 = the 2025-26 season). The sdv season key is the END year = season + 1; map at the boundary. String of digits ('2025'); the value every season parameter takes. |
Details
Unofficial and unsupported: these wrappers read the routes that cbaleague.com's own data site (a Vue single-page app) calls. The site's terms were not reviewed; check them before you republish the data. Requests are spaced at least 1 s apart per host; keep use small.
Access: every response body is AES-128-ECB encrypted with a public key shipped in the site's front-end bundle; the runtime decrypts it with
digest::AES()(digest is a suggested package: install it to use these wrappers) and refuses anything that does not decrypt. The host blocks datacenter IPs at TCP: set the environment variableCBA_PROXY(oroptions(hoopR.proxy = ...)) to a residential proxy URL; it is never printed or put in a condition. Without a proxy the request goes direct and, from a datacenter, fails within seconds (a 5 s connect timeout, no retry of a refused or timed-out connect). Where direct access works (a residential IP), setCBA_DIRECT=1for the full 60 s timeout and retries.Seasons: the API's season is the START year; every
seasonargument here is the END year (2026 = 2025-26) and is sent asseason - 1. Columns namedseasonkeep the wire value (the START year, so the sdv END year isseason + 1). The season list reaches 1995-96; box data before 2009-10 is unverified.Coverage: game wrappers serve FINISHED games only. Each first reads the game report header (cached per session), and an unfinished game raises
hoopR_asset_fetch. Box, period, roster and best-player bodies are checked against that header (points sum to the score, team ids match, advanced rows are box players). Play-by-play is served one period per request: every period is fetched, and a partial set, an unordered clock or a final score the plays do not reach is refused. Play-by-play is verified on 2017-18 and 2025-26 only and is absent for 2009-10 (hoopR_no_data). There are NO shot x/y coordinates in this API (shot location exists only as team and player zone splits), and play-by-play has no player or team ids (Chinese sentence text plus surnames). Box data has 12-minute quarters in 2009-10 and 2017-18 and 10-minute quarters in 2025-26. The women's CBA is not covered (no official source).Not game data:
cba_game_team_shots()andcba_game_team_four()answer the two teams' SEASON tables (the shot zones are the teams' current profile; the four factors are the previous season), whichever game is asked for. The reference lists (cba_team_options(),cba_team_counts()) andcba_report_status()are not echoed by the API, so their scope is unchecked.Duplicate rows: in some games (2025-26 game 100105799) the advanced-box and extras routes repeat player rows (26 rows for 12 players), and the repeats are not always copies.
cba_player_advanced()andcba_player_extras()keep the FIRST row perplayer_id.Text fields are Chinese (names, play-by-play sentences, position and nationality labels). The column descriptions here are ASCII; sportsdataverse-py keeps the full English glossaries of the codes.
Personal data: the operator ids (
createdBy/lastModifiedBy, one holds a mobile number) and any bare mobile-number string are removed from every body. Players' public bio fields (birth date, height, weight) are kept.Ids:
schedule_id(games, one global 9-digit sequence 100xxxxxx),team_id(clubs, stable across sponsor renames),player_id(persons) andmatch_type_idare integers. The roster registrationidofcba_rosters()is a 19-digit integer that an R integer or double cannot hold exactly, so it is kept as its exact digits (character).Errors are classed conditions:
hoopR_no_data(HTTP 404, a game without play-by-play),hoopR_bad_request(a non-integer id or season, HTTP 400 / 422) andhoopR_asset_fetch(any other non-200, including HTTP 500, which this API answers for a body it dislikes and which is not retried; a dead connection; an empty or undecryptable body; an unfinished game; a body for another game, team, season or day), all inheritinghoopR_error. HTTP 429 / 502 / 503 / 504 are retried, up to three attempts. Wrappers raise; they never warn and return empty.
See also
Other CBA Functions:
cba_best_player(),
cba_best_stats(),
cba_game_info(),
cba_game_team_four(),
cba_game_team_shots(),
cba_match_types(),
cba_pbp(),
cba_period_scores(),
cba_player_advanced(),
cba_player_base(),
cba_player_box(),
cba_player_defensive(),
cba_player_extras(),
cba_player_offensive(),
cba_player_shots(),
cba_report_status(),
cba_rosters(),
cba_schedule_day(),
cba_schedule_month(),
cba_team_basic(),
cba_team_counts(),
cba_team_four(),
cba_team_options(),
cba_team_overview(),
cba_team_shots(),
cba_teams()
Examples
# \donttest{
try(cba_seasons())
#> ── CBA Seasons from data-server.cbaleague.com ─────────────────── hoopR 3.2.0 ──
#> ℹ Data updated: 2026-10-31 21:27:47 UTC
#> # A tibble: 32 × 3
#> is_default key value
#> <int> <chr> <chr>
#> 1 1 2026-2027 2026
#> 2 2 2025-2026 2025
#> 3 2 2024-2025 2024
#> 4 2 2023-2024 2023
#> 5 2 2022-2023 2022
#> 6 2 2021-2022 2021
#> 7 2 2020-2021 2020
#> 8 2 2019-2020 2019
#> 9 2 2018-2019 2018
#> 10 2 2017-2018 2017
#> # ℹ 22 more rows
# }
