{"openapi":"3.1.0","info":{"title":"TRY-KartData Open Read API","version":"1.0.0","description":"Read-only API over TRY-KartData's public match data, for bots, AI tools and other services. This API publishes facts only: identities, tournaments, stages, groups, matches, rounds, results, times, live state, the track library and the dictionaries, plus counts, totals, durations and dates. Ratios, averages, dispersion, form and player ratings are not published, and no ranking is computed here (D-044, 2026-09-17). To rank or analyse across matches, take the bulk export (GET /api/open/v1/export/matches.ndjson.gz; ask the small GET /api/open/v1/export/manifest.json first) and keep it current with GET /api/open/v1/matches?since=<meta.generation>, rather than fetching each match. The former /api/v1/data family and the /api/open/v1/leaderboards/* and /api/open/v1/insights/* endpoints are retired and answer 410 Gone with a replacement path.\n\nTournaments obey the publication policy (D-049). A withdrawn or private match is removed from every tournament view (/tournaments/..., /live/tournament) as if it had not been played. A group that lost a round this way publishes no standings, because a ranking recomputed without that round was never decided. A track record falls back to the fastest entry left. Rosters carry `gameAccount` and never the collector's `sourceAccounts`. `provenance.matchId` is the public match id or null, never an internal id.\n\nErrors. Every refusal uses one envelope, `{ \"ok\": false, \"error\": { \"code\", \"message\", \"details\"? }, \"meta\" }`: 401 `data_api_auth_required` (no or empty Bearer token) or `data_api_invalid_key`; 403 `data_api_scope_denied` (the key is limited to other endpoints); 429 `open_api_rate_limited` (the per-key quota, 600 requests per minute, with `Retry-After` and `X-RateLimit-*`) or `data_api_auth_rate_limited` (too many failed attempts from one address); 503 `data_api_no_keys_configured`, `replica_data_refreshing` (retry in a few seconds) or an endpoint's own unavailability. The codes are the strings earlier versions sent as `error`; read `error.code`.\n\nCORS: none. The API sends no `Access-Control-*` headers and answers no preflight, so a page on another origin cannot read it. Call it from a server: a key is a secret and must not ship in a web page or an app bundle.\n\nVersioning and deprecation. `/api/open/v1` is the major version and is served as long as v1 exists. `info.version` follows semver for this contract: patch for wording, minor for additions (endpoints, fields, enum values; clients must ignore what they do not know), major for removing an endpoint or field or changing what one means. Before removal a field or endpoint is marked deprecated here (`deprecated: true` and a note with the date and the replacement) for at least 90 days, and the holders of the keys that used it in that time are told. The exception is a field that breaks the facts-only rule (D-044) or exposes private data: it is removed at once and listed in the change log. A retired endpoint answers 410 Gone with its replacement for at least 180 days."},"servers":[{"url":"https://openapi.tryleague.net","description":"Production (the replica host)"}],"security":[{"bearerAuth":[]}],"paths":{"/api/open/v1/meta/version":{"get":{"summary":"Get Open Read API version metadata","security":[],"responses":{"200":{"description":"Open Read API metadata"}}}},"/api/open/v1/meta/capabilities":{"get":{"summary":"Get capability groups and first-wave endpoints","security":[],"responses":{"200":{"description":"Open Read API capability metadata"}}}},"/api/open/v1/meta/sync-health":{"get":{"summary":"Read PostgreSQL replica freshness and per-stream coverage","responses":{"200":{"description":"Replica mode, status, missing streams, stale streams, revisions, source timestamps, replica timestamps, and lag."},"401":{"description":"Bearer token is required."},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/openapi.json":{"get":{"summary":"Download the machine-readable OpenAPI document","security":[],"responses":{"200":{"description":"OpenAPI JSON document"}}}},"/api/open/v1/live/match":{"get":{"summary":"Read the current live match summary","description":"Poll at most once per second and always send back the previous ETag. The ETag is derived from the live-state revision (not from the response bytes), so an unchanged generation answers 304 with no body and no recomputation. Responses carry Cache-Control: public, max-age=1 and X-Suggested-Poll-Seconds: 1; intermediate caches and CDNs may serve them. Use conditional requests: a poll without If-None-Match transfers the full body every time. Clients built on Node's fetch (undici) need one extra step — under the default cache mode a request that carries If-None-Match is downgraded to no-store and fetch then appends Cache-Control: no-cache, so this endpoint can never answer 304. Pass an explicit cache option (cache: 'force-cache'), or set your own Cache-Control request header, so that header is not appended. meta.generatedAt and meta.replica still describe this individual response and therefore do not participate in the ETag, which is why the validator is weak. `adjustmentLedger[]` 与归档明细走同一套白名单：只发 `playerId` / `delta` / `roundKey` / `roundLabel` / `createdAt` / `countsInSettlement`（是否计入最终结算）/ `reason`（理由文字自 2.35.0 起公开）与 `hasReason`。 Join keys (D-046): `rounds.history[]`, `teamMatch.history[]` and `teamMatch.teamRounds[]` may carry `fairplayRoundKey`; `players[]`, the `results[]` of `rounds.history[]` and `rounds.stagingRound`, and `teamMatch.teamRounds[].players[]` may carry `playerKey`. Both are absent when they cannot be determined. Track ids (D-051, 2.34.0): `rounds.history[]`, `rounds.stagingRound` and `teamMatch.teamRounds[]` may carry `trackId`, the same id as `items[].id` of GET /api/open/v1/tracks, matched from the round's track name (the library name exactly, then the same name normalized); absent when no single library track matches. Not published (2.34.0, D-044): `thisRoundSpread` (slowest minus fastest finish, a spread; `thisRoundFastest` and `thisRoundSlowest` are still published), `personalBestInMatch` (career best times are a derived statistic), and a `roundHighlight` of type `closest_battle` (its text carried the smallest score gap, shown only under a threshold): such a round is published as `steady_lead` (the leader did not change), so `roundHighlight` is `lead_change` or `steady_lead`.","parameters":[{"name":"If-None-Match","in":"header","required":false,"description":"Previous weak ETag. Matching generations answer 304 Not Modified.","schema":{"type":"string"}}],"responses":{"200":{"description":"Current match summary including a lightweight trackPool summary with trackPoolName, currentTrack, currentSelection, remainingTracks, and lastPlayedTrack.","headers":{"ETag":{"description":"Weak validator derived from the live-state revision, the version vector, and the publication policy.","schema":{"type":"string"}},"Cache-Control":{"description":"public, max-age=1","schema":{"type":"string"}},"X-Suggested-Poll-Seconds":{"description":"Shortest polling interval that can still observe new data: 1.","schema":{"type":"string"}}}},"304":{"description":"The live-state generation still matches the supplied If-None-Match. No body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The live match exists but is not public."},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to build the live match summary."},"503":{"description":"The live state is temporarily unavailable."}}}},"/api/open/v1/live/tournament":{"get":{"summary":"Read the active tournament snapshot","description":"Returns the active tournament and stage, all groups or one group selected by groupId, A-P broadcast labels, P1-P8 player identities and competition accounts, standings, completed rounds, ordered track directory, record history, and data-quality warnings.","parameters":[{"name":"groupId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current tournament statistics snapshot."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The requested tournament group was not found."},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"Tournament statistics are unavailable."}}}},"/api/open/v1/tournaments":{"get":{"summary":"List every tournament and stage independently of the live binding","responses":{"200":{"description":"Bounded tournament and stage catalog."},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"description":" 从 2026-09-14 起，含对决阶梯（`bo9-duel-series`）的赛事额外带 `officialResult`：冠亚季军来自阶段规则里的 `duelSequence` 与 `routes`，逐环用规则引擎判定并按游戏账号跨场认人。阶梯对不上或有未决对决时输出 `provisional` 且不给 `podium`，不发布未经核实的领奖台。 **缓存**：ETag 由历史代号与赛事规则摘要派生，`Cache-Control: public, max-age=5`。","parameters":[{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"}]}},"/api/open/v1/tournaments/{tournamentId}/stages/{stageId}":{"get":{"summary":"Read one explicit tournament stage with all groups and evidence","parameters":[{"name":"tournamentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explicit stage projection."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Tournament or stage was not found."},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/tournaments/{tournamentId}/stages/{stageId}/groups/{groupId}":{"get":{"summary":"Read one explicit tournament group including P1-P8 identities","parameters":[{"name":"tournamentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stageId","in":"path","required":true,"schema":{"type":"string"}},{"name":"groupId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explicit group projection."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Tournament, stage, or group was not found."},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/players":{"get":{"summary":"List canonical players for bot/AI lookup","responses":{"200":{"description":"Player listing. data.items[] holds identity plus career counts only."},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"description":"Player directory. Each row carries playerKey, playerName, aliases, globalId and the career counts totalMatches / totalWins (with the firstPlaceCount alias). The mean finishing position (avgRank) was removed on 2026-09-17 — averages are not published (D-044). **缓存**：ETag 由历史代号派生，`Cache-Control: public, max-age=5`。","parameters":[{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"}]}},"/api/open/v1/players/{playerKey}":{"get":{"summary":"Read one player profile by canonical player key or display name","description":"One player profile, projected through an allowlist: playerKey, primaryName, aliases, globalId, matchHistory and the career block of counts and dates (totalMatches, totalWins, totalPodiums, firstPlaceCount, totalRounds, totalScore, totalScoreComplete, scoreRoundsMissing, activeDays (distinct match days, Asia/Shanghai), firstMatchDate, lastMatchDate). Nothing else is published. Removed on 2026-09-17 (D-044): averages (avgRank, avgScore, avgTimeMs), rates (the placement rates plus finish and retire rate), consistency (rankStdDev and the stability score), recentForm, records, rankDistribution, per-track stats and headToHead. A field that is not on the allowlist does not leave, so a new field on the player record is internal by default. Compute rankings and comparisons yourself from the bulk export, kept current with `?since=`. **缓存**：ETag 由历史代号派生，`Cache-Control: public, max-age=5`。","parameters":[{"name":"playerKey","in":"path","required":true,"schema":{"type":"string"}},{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"}],"responses":{"200":{"description":"Player profile"},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The requested player profile was not found."},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the player profile."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/matches":{"get":{"summary":"List archived matches with lightweight summaries","description":"Returns public historical match summaries. Duo/trio summaries include teams[] and winnerTeams[] so clients can identify teammates and spray-color teams without inferring from rank order. 从 2026-09-15 起，响应的 `data.nextCursor` 与 `meta.pagination` 支持游标分页：`meta.pagination.mode` 为 `cursor` 或 `offset`，`hasMore` 说明还有没有下一页，`versionDrift=true` 表示这一轮翻页途中底层数据变过（有人改分、拆身份或改规则），不是错误，但消费方应据此决定接着读还是从头重来。offset 分页保持原样，老消费方不受影响。 **缓存**：ETag 由历史代号与本次查询参数派生，`Cache-Control: public, max-age=5` —— 列表在换代时要快，刚封存的那场要马上出现。","parameters":[{"name":"mode","in":"query","required":false,"description":"Optional mode filter.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Title keyword filter.","schema":{"type":"string"}},{"name":"player","in":"query","required":false,"description":"Player keyword filter.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Same-track filter after whitespace-insensitive normalization.","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"Pagination offset.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Pagination limit.","schema":{"type":"integer"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"游标分页：把上一页 `data.nextCursor` 原样传回来。列表按结束时间倒序，变动都发生在最前面，纯 offset 分页在直播中会重复发送（新封存一场）或静默跳过（撤回一场）记录；游标记的是「上一页最后一条是谁」，不受影响。与 offset 同时给出时游标优先。游标绑定了筛选条件，换了 mode/player/track/q 还沿用旧游标会返回 400 `cursor_filter_mismatch`。"},{"name":"snapshotId","in":"query","required":false,"schema":{"type":"string"},"description":"把这次读取钉在某一代公开语料上。句柄取自 `data.snapshotId` 或 `meta.version.snapshotId`。钉住且仍然有效时 `meta.version.consistency` 为 `verified_snapshot`，并附 `verifiedSnapshot`；该代际已经换掉则返回 **409 `snapshot_expired`**（`details` 带 `requestedSnapshotId` 与 `currentSnapshotId`），请不带 snapshotId 从头读。**旧代际不保留**（`retained: false`）—— 公开 API 由影子副本服务，一代历史压缩后 34 MB，副本刷新时是先清旧代再拉新代的，同时放两代会顶破内存上限；影子库里每条流也只留最新一代。所以这个参数的保证是「要么给你那一代，要么拒绝」，不是「服务端替你留着」。同一个参数在 /api/v1/data 上含义完全相同。"},{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"},{"name":"since","in":"query","required":false,"schema":{"type":"string"},"description":"增量同步。把上一次响应里的 `meta.generation`(这一代公开语料的代号)原样传回来,只拿这一代之后**新增、更正与撤回**的那些。响应形状不同于全量列表:`data.mode` 为 `incremental` 或 `full_resync`;`data.matches[]` 是变更集(每条摘要多一个 `revision`),`data.removed[]` 是墓碑(`{id, withdrawn:true, withdrawnAt}`,消费方据此删掉本地缓存),`data.generation` 是这次的新水位线。分页照旧用 `limit` 与 `cursor`;墓碑不分页(删除是幂等的,每一页都带着没有副作用)。\n\n**代号不认得时给全量**:`mode=full_resync` + `resyncReason=since_generation_unknown`。服务端只记得最近若干代(进程内,重启即空);猜一个差集出来会把你送进静默的数据缺口,所以它如实说不认得。收到 `full_resync` 就把本地那份整份换掉。"}],"responses":{"200":{"description":"Match list"},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the match list."},"503":{"description":"这个部署没有历史代际可追踪,答不了 ?since=(`incremental_sync_unavailable`)。"}}}},"/api/open/v1/matches/{matchId}":{"get":{"summary":"Get one archived match by id","description":"Returns one public historical match detail. Details include event metadata, duo/trio team context, teammate ids, spray colors, Chinese-readable finishTimeText fields, and title phrases such as 35图 or 抢300 that must be interpreted with the terminology dictionary instead of being paraphrased as stages. `resultSemantics` carries the rule-derived `officialResult` (status/basis/ruleId/ruleVersion), `scoreLeader` (may tie, carries a complete flag), `seriesScore` (confirmed wins and undecided rounds), `competition` (discipline/format/bestOf/winsRequired/targetPoints) and, for 4v4, `displaySetScore`. Each round carries `validity`, `timeRankComplete` and `scoreRankComplete`; each result carries `timeRank`, `scoreRank` and the four availability dimensions. `winner`, `rank`, `officialRank` and `score` are unchanged; `winner` is still the leader by total score, not the official result — read `resultSemantics.officialResult` for that. From 2026-09-17 (D-044) `players[].stats` is an allowlist: the match total `totalScore` with the two flags that qualify it (`totalScoreComplete`, `pointsMissingRounds`) plus the round counts `roundsPlayed`, `roundsFinished` and `roundsRetired`. From facts-4 (2.35.0) every match has two results. `totalScore` is the official result (结算成绩): round points plus the adjustments with `countsInSettlement: true` (「计入最终结算」, set by the operator per entry, e.g. a forfeit), and `finalRank` is ranked on it. The adjusted result (加减分后成绩) is `bonusTotal` (sum of the positive entries that do not count), `deductionTotal` (sum of the negative ones, zero or negative) and `adjustedTotal` = totalScore + bonusTotal + deductionTotal = round points + every entry; no adjusted rank is published. `players[].adjustments[]` lists that player's entries (`delta`, `countsInSettlement`, `reason`, `createdAt`, `roundKey`), oldest first. Read `totalScore` for what a driver scored in this match, but check `totalScoreComplete` before ranking on it — when a round was archived without points, `totalScore` is the known subtotal rather than the full total. No longer published: finishRate, rtRate, avgRank, avgScore, avgTimeMs, rankStdDev, rankVariance, consistencyScore, the personal peaks, the streaks and the per-track stats. Each round result is an allowlist too — identity, rank, officialRank, score, finish time and status plus the result-semantics fields — so zScore, gapToTop, gapToPrev, leadToSecondMs, tierGroup and the intensity fields are gone, and `rounds[].roundStats` keeps only fastestTimeMs, slowestTimeMs, finishedCount and retiredCount. Per-round scores, ranks and times are unchanged. **缓存**：ETag 由**这一场的内容摘要**派生(公开投影版本号 + 记录 + 赛事身份),不再按响应体哈希现算 —— 所以**别人归档一场新比赛不会让这一场的 ETag 变**,你手上那份继续有效。同一个摘要也写在 `/api/open/v1/export/manifest.json` 的每条 `digest` 上,并且就是 `?since=` 判断「变没变」的依据,三条路必然一致。`Cache-Control: public, max-age=3600, stale-while-revalidate=86400`。归档记录一旦入库就不再变，任何更正都会换代、换 ETag，所以带上 `If-None-Match` 是安全的：内容变了你一定会拿到新版。 **改分账本与赛事绑定是白名单**（2026-09-17，v2.26.1 线上扫描）：`adjustmentLedger[]` 每条只发 `playerId` / `delta` / `roundKey` / `roundLabel` / `createdAt` / `countsInSettlement`（是否计入最终结算）/ `reason`（理由文字自 2.35.0 起公开）与 `hasReason`；条目自己的 uuid（`id`）与采集证据的内部 id（`roundEvidenceId`）不再发布。`tournamentMatchBinding` 只发 `tournamentId` / `stageId` / `groupId` / `kind`；`matchContextId`、`coordinationOperationId`、`drawSessionId`、`tiebreakRequestId`、`groupRevision`、`candidateIds`、`unresolvedParticipantIds`、`participantIdMap`(内部身份映射) 与 `boundAt` 不再发布。赛事身份本身没有变 —— 那四个 id 就是在 /api/open/v1/tournaments 上查得到的那几个。 Join keys (D-046): rounds may carry `fairplayRoundKey` (hex, the identity the Fairplay bridge sent for that round, to join Fairplay round bundles); match players and round results may carry `playerKey` (the same value as `/players` `playerKey`). Both are absent when they cannot be determined. Track ids (D-051, 2.34.0): a round may carry `trackId`, the same id as `items[].id` of GET /api/open/v1/tracks, matched from the round's track name (the library name exactly, then the same name normalized); absent when no single library track matches. `players[].stats.totalTimeMs` is the sum, in milliseconds, of the driver's finish times over the rounds they finished in this match (a retired round adds nothing); null when they finished none. It is a total, not a pace: compare it only between drivers with the same `roundsFinished`.","parameters":[{"name":"matchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"snapshotId","in":"query","required":false,"schema":{"type":"string"},"description":"把这次读取钉在某一代公开语料上。句柄取自 `data.snapshotId` 或 `meta.version.snapshotId`。钉住且仍然有效时 `meta.version.consistency` 为 `verified_snapshot`，并附 `verifiedSnapshot`；该代际已经换掉则返回 **409 `snapshot_expired`**（`details` 带 `requestedSnapshotId` 与 `currentSnapshotId`），请不带 snapshotId 从头读。**旧代际不保留**（`retained: false`）—— 公开 API 由影子副本服务，一代历史压缩后 34 MB，副本刷新时是先清旧代再拉新代的，同时放两代会顶破内存上限；影子库里每条流也只留最新一代。所以这个参数的保证是「要么给你那一代，要么拒绝」，不是「服务端替你留着」。同一个参数在 /api/v1/data 上含义完全相同。"},{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"}],"responses":{"200":{"description":"Match detail"},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"请求的比赛不存在，或从来没有公开过。已经公开过又撤回的请看 410。"},"410":{"description":"这场比赛公开过、现已撤回。错误码 `match_withdrawn`，`details` 带 `matchId` 与 `withdrawnAt`。收到它请删掉本地缓存的那一份 —— 与 404（从不存在，或从未公开）是不同的含义。"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the match detail."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/tracks":{"get":{"summary":"List the track library with localized names and metadata","description":"The track library is the game’s own data: name and translations, theme, laps, length, star rating, the four race modes, regional availability and the cover image. Nothing here is derived from match records. Responses carry a weak ETag and Cache-Control: public, max-age=300 — the library changes every few weeks, so send back the previous ETag and take the 304. The ETag covers the query as well, so a filtered request never validates against the full list.","parameters":[{"name":"lang","in":"query","required":false,"description":"Localization target. Requests may pass global language codes such as zh-Hans, zh-Hant, en, ko, or ja, but track translation objects themselves only expose zh-Hans, zh-Hant, and ko and otherwise fall back to zh-Hans.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Keyword search across track names and themes.","schema":{"type":"string"}},{"name":"theme","in":"query","required":false,"description":"Filter by theme key or localized theme name (Simplified Chinese, Traditional Chinese, or Korean).","schema":{"type":"string"}},{"name":"If-None-Match","in":"header","required":false,"description":"Previous weak ETag. An unchanged library answers 304 Not Modified.","schema":{"type":"string"}}],"responses":{"200":{"description":"Localized track catalog. data.items[] carries id, name, displayName, translations, theme fields, laps, lengthKm, stars, raceMode/itemMode/reverseMode/crazyMode/randomOnly and availableCn/availableKr/availableTw."},"304":{"description":"The track library has not changed since the ETag you sent."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the track catalog."},"503":{"description":"The track library is not available on this host."}}}},"/api/open/v1/dictionaries":{"get":{"summary":"List the public dictionary namespaces","responses":{"200":{"description":"Dictionary namespace index. Only players, tracks and terminology are published; the internal metrics glossary is not."},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the dictionary namespaces."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"description":"**缓存**：ETag 由翻译表的代际派生，`Cache-Control: public, max-age=300`。","parameters":[{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"}]}},"/api/open/v1/dictionaries/{namespace}":{"get":{"summary":"Read one public dictionary namespace: players, tracks or terminology","parameters":[{"name":"namespace","in":"path","required":true,"description":"Dictionary namespace name.","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"Localization target for displayName values.","schema":{"type":"string"}},{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。内容没变就是 **304 Not Modified**，零字节，而且服务端连响应体都不会构造。"}],"responses":{"200":{"description":"Dictionary namespace entries. terminology is the competition wording glossary for 局/小局/场/比赛/轮次, 35图, 抢300 and RT/未完成, and may expose preferredPhrasing plus avoidPhrasing metadata."},"304":{"description":"Not modified. 上一次的 ETag 仍然有效 —— 内容没有变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No such public namespace. The internal metrics glossary answers 404 here as well — the public surface does not confirm whether it exists. details.publicNamespaces lists what is available."},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the dictionary namespace."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}},"description":"**缓存**：ETag 由翻译表的代际、命名空间与语言派生，`Cache-Control: public, max-age=300`。"}},"/api/open/v1/overview":{"get":{"summary":"Read global history overview metrics","responses":{"200":{"description":"Global overview: playerCount, totalMatches, totalRounds and latestMatches. The wins and activity leaderboards were removed on 2026-09-17 — this API computes no rankings (D-044)."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the overview."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/live/track-pool":{"get":{"summary":"Read the current live track-pool detail","responses":{"200":{"description":"Current live track-pool detail with trackPoolName, remaining tracks, used tracks, currentSelection, and pending resolutions."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The live match exists but its track-pool context is not public."},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to build the live track-pool detail."},"503":{"description":"The live state is temporarily unavailable."}}}},"/api/open/v1/track-pool-templates":{"get":{"summary":"List public track-pool templates","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Optional fuzzy template-name query. Accepts human phrases such as G风哥图池 or X哥图池."}],"responses":{"200":{"description":"Public track-pool template library with trackPoolName, trackCount, updatedAt, and optional query by name."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the public track-pool template library."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/track-pool-templates/{templateKey}":{"get":{"summary":"Read one public track-pool template detail","parameters":[{"name":"templateKey","in":"path","required":true,"schema":{"type":"string"},"description":"Template id or fuzzy human name such as G风哥图池. Fuzzy name lookup resolves to the best public template detail."}],"responses":{"200":{"description":"Public track-pool template detail with tracks array, rawText, lookup metadata, and fuzzy name lookup support."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The requested track-pool template was not found."},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the public track-pool template detail."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/matches/withdrawn":{"get":{"summary":"List matches that were published and have since been withdrawn","description":"撤回清单。消费方一次调用就能把本地缓存对齐，不用挨个 id 去探 410。只列「公开过之后又撤回」的比赛（可见性策略里逐条写下 `matchId` 且带 `withdrawnAt` 的那些）；从来没打算公开的比赛不在此列 —— 没有任何消费方缓存过它们，列出 id 只会白白透露存在性。","responses":{"200":{"description":"`{ count, items: [{ matchId, withdrawnAt, note }] }`"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"description":"The server failed to load the withdrawal list."},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/api/open/v1/export/manifest.json":{"get":{"summary":"Read the bulk export manifest","description":"全量导出的清单,很小 —— **先问它,再决定要不要拉那个大文件**。内容:`generation`(这一代的代号)、`projectionVersion`(公开投影版本号)、`generatedAt`、`matchCount`(场次数,恒等于导出文件的行数)、`file`(名字/类型/字节数/sha256)、`matches[]`(每场 `id` + `revision` + 内容指纹 `digest`)。指纹和 `GET /matches?since=` 用的是同一个,所以拿 manifest 也能自己算差集。带 ETag 与 `Cache-Control: public, max-age=60`。","parameters":[{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。没换代就是 **304 Not Modified**，零字节。"}],"responses":{"200":{"description":"The export manifest for the current generation."},"304":{"description":"Not modified. 没换代 —— 导出文件也没变。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"这个部署不发布全量导出(`export_unavailable`),或者第一份还没生成出来(`export_not_ready`)。"}}}},"/api/open/v1/export/matches.ndjson.gz":{"get":{"summary":"Download every archived match detail as one gzipped NDJSON file","description":"全量导出:每行一场,**与 `GET /api/open/v1/matches/{matchId}` 完全同一投影、同一白名单**,去掉信封之后逐字节相同(有测试随机抽样比对钉着这一条)。实测 320 场约 6.8 MB(gzip)。\n\n这是「算榜单 / 做离线分析」的正路:一次拿全,之后靠 `GET /api/open/v1/matches?since=<generation>` 增量,**不要逐场 320 次问明细**。\n\n响应带 `Content-Encoding: gzip`,`Content-Type: application/x-ndjson` —— 会自动解压的客户端(fetch、`curl --compressed`)拿到的就是 NDJSON 文本;要原始 .gz 的自己保存即可。行的顺序照列表口径(结束时间倒序,同时间按 id 倒序),同一份数据两次导出产出同一个文件。\n\n文件在每次历史换代之后重新生成,ETag 随之改变;没换代就是 304。响应头 `X-Fenpai-Export-Generation` 给的是代号,不解析 manifest 也判断得了手上那份是不是这一代。 Each line carries the same join keys and round `trackId` as `GET /matches/{id}`.","parameters":[{"name":"If-None-Match","in":"header","required":false,"schema":{"type":"string"},"description":"把上一次响应的 ETag 原样传回来。没换代就是 **304 Not Modified**，零字节。"}],"responses":{"200":{"description":"Gzipped NDJSON, one public match detail per line."},"304":{"description":"Not modified. 没换代 —— 文件没变,不用再拉一次 6.8 MB。"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"这个部署不发布全量导出(`export_unavailable`),或者第一份还没生成出来(`export_not_ready`)。"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key"}},"schemas":{"SuccessEnvelope":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","const":true},"data":{"type":"object"},"meta":{"$ref":"#/components/schemas/Meta"},"links":{"type":"object"}}},"ErrorEnvelope":{"type":"object","required":["ok","error","meta"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object"}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"Meta":{"type":"object","required":["apiVersion","generatedAt"],"properties":{"apiVersion":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"},"analysisScope":{"type":"string"},"source":{"$ref":"#/components/schemas/SourceMeta"},"citations":{"type":"array","items":{"$ref":"#/components/schemas/Citation"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"},"replica":{"$ref":"#/components/schemas/ReplicaStatus"},"version":{"type":["object","null"],"description":"四维版本向量。`generatedAt` 只说明响应何时生成，不说明内容是哪一代 —— 翻页途中数据被改过，靠时间戳看不出来。取不到的维度是 null，不编一个出来。","properties":{"history":{"type":["integer","null"],"description":"成绩事实的修订号"},"identity":{"type":["integer","null"],"description":"身份与生涯统计的修订号"},"rules":{"type":["string","null"],"description":"赛事与阶段规则的摘要；任一赛事/阶段 revision 变化即变"},"publication":{"type":["string","null"],"description":"公开可见性策略 + 源修订号组成的代际标识；公开撤回或策略变更会换掉它"},"consistency":{"type":"string","enum":["best_effort","verified_snapshot"],"description":"`verified_snapshot` = 客户端传了 snapshotId，服务端核对过整份响应确实来自那一代；`best_effort` = 没钉，可能横跨代际。此前这个值叫 `frozen`，那个词会被读成「服务端替我把这一代留着了」，而我们并没有——改名是为了不让一个词去许一个没有的承诺。"},"verifiedSnapshot":{"type":["object","null"],"description":"只在 consistency 为 verified_snapshot 时出现。`guarantee: single_generation_or_refused` = 要么整份响应都来自你指定的那一代，要么这次请求被拒绝；`retained: false` = 我们**没有**替你把那一代留着，它随时可能换掉，换掉后你会收到 409 snapshot_expired 而不是一份混着两代的数据。","properties":{"snapshotId":{"type":"string"},"guarantee":{"type":"string","enum":["single_generation_or_refused"]},"retained":{"type":"boolean"},"verifiedAt":{"type":"string","format":"date-time"}}},"versionDrift":{"type":"boolean","description":"只在没钉代际、且构造这份响应的过程中确实换过代时出现。不是错误，但这一份可能横跨两代。"},"snapshotId":{"type":["string","null"],"description":"当前公开语料代际的句柄；可回传到 `?snapshotId=` 把读取钉住。两个公开面（/api/open/v1 与 /api/v1/data）都认这个参数。"}}},"generation":{"type":["string","null"],"description":"这一代公开语料的代号（不透明摘要）。把它记下来，下次带 `GET /api/open/v1/matches?since=<代号>` 回来就只拿这一代之后新增、更正与撤回的那些，不用重拉全量。没有历史数据可读时为 null。它和 `meta.version.publication` 说的是同一件事，只是这一个是给增量同步用的稳定入口。"}}},"SourceMeta":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"confidence":{"type":"number"}}},"ReplicaStatus":{"type":"object","required":["mode","enabled","source","isReplica"],"properties":{"mode":{"type":"string","enum":["primary","replica"]},"enabled":{"type":"boolean"},"source":{"type":"string","enum":["json","postgresql"]},"isReplica":{"type":"boolean"},"status":{"type":"string","enum":["healthy","warming","stale","unavailable"]},"isStale":{"type":"boolean"},"checkedAt":{"type":"string","format":"date-time"},"staleAfterMs":{"type":"integer"},"heartbeatAgeMs":{"type":["integer","null"]},"revision":{"type":["string","null"]},"sourceRevision":{"type":["integer","null"]},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time"},"replicatedAt":{"type":["string","null"],"format":"date-time"},"lagMs":{"type":["integer","null"]},"missingStreams":{"type":"array","items":{"type":"string"}},"staleStreams":{"type":"array","items":{"type":"string"}},"streams":{"type":"array","items":{"$ref":"#/components/schemas/ReplicaStreamStatus"}}}},"ReplicaStreamStatus":{"type":"object","required":["stream","available","isStale"],"properties":{"stream":{"type":"string"},"available":{"type":"boolean"},"isStale":{"type":"boolean"},"revision":{"type":["string","null"]},"sourceRevision":{"type":["integer","null"]},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time"},"replicatedAt":{"type":["string","null"],"format":"date-time"},"lagMs":{"type":["integer","null"]},"ageMs":{"type":["integer","null"]},"semanticHash":{"type":["string","null"]}}},"Citation":{"type":"object","properties":{"kind":{"type":"string"},"id":{"type":"string"},"label":{"type":"string"}}},"PaginationMeta":{"type":["object","null"],"properties":{"offset":{"type":"integer"},"limit":{"type":["integer","null"]},"total":{"type":"integer"}}},"ResultSemantics":{"type":"object","description":"结果语义投影。把「正式赛果」「积分领先者」「两种排序」分开，各自声明依据与完整性。契约见 docs/reviews/2026-09-14/semantic-contract.md。","properties":{"semanticVersion":{"type":"string","enum":["result-contract-2"]},"competition":{"type":"object","properties":{"discipline":{"type":"string","enum":["individual","duo","trio","team4v4"]},"format":{"type":"string","description":"head_to_head / team_series / points"},"resultUnit":{"type":"string","enum":["winner","ranking"],"description":"winner=有唯一胜者；ranking=结果是名次，没有单一冠军"},"ruleId":{"type":["string","null"]},"ruleVersion":{"type":["integer","null"]},"stageKind":{"type":["string","null"]},"bestOf":{"type":["integer","null"]},"winsRequired":{"type":["integer","null"]},"targetPoints":{"type":["integer","null"]}}},"officialResult":{"type":"object","properties":{"status":{"type":"string","enum":["unknown","provisional","confirmed","superseded"]},"winnerEntryIds":{"type":["array","null"],"items":{"type":"string"},"description":"空数组不表示未知或平局；未知一律 null"},"basis":{"type":["string","null"],"enum":["rules_engine","referee_decision","legacy_unverified",null]},"ruleId":{"type":["string","null"]},"ruleVersion":{"type":["integer","null"]},"resultRevision":{"type":["integer","null"]},"sourceRevision":{"type":["integer","null"]},"disagreesWithRulesEngine":{"type":"boolean","description":"归档里的胜者与规则复算结果不一致时出现"}}},"scoreLeader":{"type":"object","description":"某个积分口径下的领先对象，可以并列。**不叫冠军。**","properties":{"entryIds":{"type":["array","null"],"items":{"type":"string"}},"basis":{"type":"string","enum":["recorded_round_points","confirmed_set_wins"]},"complete":{"type":"boolean","description":"false 表示有人的积分不齐，这个排序不能当完整排名用"}}},"seriesScore":{"type":["object","null"],"properties":{"confirmedWins":{"type":"object","additionalProperties":{"type":"integer"}},"unresolvedRounds":{"type":"integer"},"storedWins":{"type":["object","null"],"description":"4v4：封存时写下的聚合盘数"},"storedAgreesWithRounds":{"type":"boolean"}}},"displaySetScore":{"type":["object","null"],"description":"4v4 盘内比分。只给数字无法判断该不该清零，所以带上第几盘与是否已结束。","properties":{"setNumber":{"type":"integer"},"setClosed":{"type":"boolean","description":"true = 这是那一盘的结束比分，下一盘从 0:0 开始"},"smallScore":{"type":"object","additionalProperties":{"type":"integer"}},"pointsToTakeRound":{"type":["integer","null"]}}}}},"TournamentOfficialResult":{"type":"object","description":"赛事级正式结果。冠军不属于任何一场比赛，它是从对决阶梯传导出来的。","properties":{"status":{"type":"string","enum":["unknown","provisional","confirmed","superseded"]},"basis":{"type":["string","null"],"enum":["rules_engine","referee_decision","legacy_unverified",null]},"ruleId":{"type":["string","null"]},"ruleVersion":{"type":["integer","null"]},"stageId":{"type":"string"},"unresolvedDuels":{"type":"integer"},"podium":{"type":["array","null"],"items":{"type":"object","properties":{"routeId":{"type":"string","description":"champion / runner-up / third-place"},"rank":{"type":["integer","null"]},"entryId":{"type":"string"},"entryLabel":{"type":"string"},"decidedByMatchId":{"type":["string","null"]}}}}}},"Pagination":{"type":"object","properties":{"mode":{"type":"string","enum":["offset","cursor"]},"offset":{"type":"integer"},"limit":{"type":["integer","null"]},"total":{"type":"integer"},"hasMore":{"type":"boolean"},"nextCursor":{"type":["string","null"],"description":"下一页的游标；没有下一页时为 null"},"versionDrift":{"type":"boolean","description":"true = 本轮翻页横跨了一次数据变更"}}}},"responses":{"Unauthorized":{"description":"`data_api_auth_required` (no or empty Bearer token) or `data_api_invalid_key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"`data_api_scope_denied`: this key is limited to other endpoints.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"TooManyRequests":{"description":"`open_api_rate_limited` (per-key quota) or `data_api_auth_rate_limited` (too many failed attempts from this address).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"headers":{"Retry-After":{"description":"Seconds until the next window.","schema":{"type":"string"}}}},"ServiceUnavailable":{"description":"`data_api_no_keys_configured`, `replica_data_refreshing` (retry in a few seconds) or this endpoint's own unavailability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}