# GoalXData Football API

## 1. 统一合同

- 基础地址：`https://goalxdata.com/api/v1/football`
- 鉴权：`Authorization: Bearer <API_KEY>` 或 `X-API-Key`
- 比赛、赛事、赛季、球队、球员分别使用 `gxf_`、`gxc_`、`gxs_`、`gxt_`、`gxp_` 公共编号。
- 客户响应不包含采集方名称、原始编号、内部数据库编号或原始载荷。
- 缺失数据保持 `null` 或明确的不可用状态；不补零、不推算。

## 2. 北京时间

所有客户 API 使用北京时间：

- `meta.timezone = "Asia/Shanghai"`
- `meta.utcOffset = "+08:00"`
- 所有 `date-time` 字段使用带 `+08:00` 的 ISO 8601 字符串。
- `date=YYYY-MM-DD` 按北京时间自然日解释。

系统内部以 UTC 绝对时间保存，客户输出时转换为北京时间，因此不会改变同一场比赛的真实时间点。

## 3. 高级赔率

高级赔率是独立附加包，并进一步拆成两个互不自动包含的产品：“高级赛前赔率”与“滚球赔率”。历史赔率是第三个独立附加包；高级详情基础包不会自动获得任何赔率权限。已签发旧 Key 继续按原 scope 兼容，不改变既有权益。

| 接口 | 用途 |
| --- | --- |
| `GET /pregame-index` | 多场高级赔率 |
| `GET /matches/{gxf_id}/pre-match-index` | 单场高级赔率 |
| `GET /matches/{gxf_id}/pre-match-index/opening-closing` | 历史赔率：出盘与临场（独立权限） |
| `GET /matches/{gxf_id}/pre-match-index/history` | 旧 Key 全部已观测历史兼容接口（已弃用） |
| `GET /matches/{gxf_id}/in-play-index` | 单场滚球赔率（需要独立权限） |

新客户需要特别注意：高级赛前赔率 REST 返回的是“你请求这一刻的完整当前快照”，不是该场从出盘到现在的每一次变盘记录。客户可以每 2 秒读取最新快照，或订阅 `prematch` WebSocket 实时接收后续变化并在自己的系统中保存。新合同的历史赔率附加包只返回出盘与临场；旧版 `/history` 仅供已经保留旧 scope 的 Key 兼容，且只代表 GoalXData 实际观测并保存的变化，不代表上游所有瞬时变化。

高级赔率遵守以下硬规则：

- 只返回已经与 GoalXData 比赛唯一配对的数据。
- 只返回通过新鲜度校验的正式快照。
- 单场默认一次返回该时点的完整当前快照：市场中实际可用的公司简称、市场、全场与半场指数及赔率。
- 产品覆盖口径为 18 家公司，但逐场受市场可用性影响，可能不齐全；这不是固定对外代码表，也不保证每场恰好返回 18 家。
- 公司只返回 GoalXData 英文简称，不返回公司全名或上游编号。
- 字段口径统一为“市场 → 指数 → 赔率”：`marketName` 是市场，`index` 是指数（例如 `2.5`），`priceDecimal` 是赔率。
- 历史赔率附加包只返回出盘与临场两个边界快照，仅包含全场进球数、全场让球盘和全场胜平负；不返回中间变化。
- 历史赔率只承诺上游实际提供并已保存的数据；缺少任一边界时明确不可用，不推算、不补零、不合成。
- 当前市场为胜平负、亚洲让球和大小球；某场没有真实值时不生成。
- 主客队发生反向配对时，球队方向、胜负方向和亚洲让球方向同步反转。
- 开赛后不再返回该场赛前高级赔率。
- 赛程修订等待重新核验时暂停返回；暂停不算抓取失败，核验完成后自动恢复。

可选查询参数：

- `date=YYYY-MM-DD`
- `competitionId=gxc_...`
- `matchId=gxf_...`
- `marketKeys=moneyline,asian_handicap,goals_over_under`
- `mainLinesOnly=true|false`，默认 `false`
- `maxLinesPerBookmaker=1..50`
- `limit=1..200`

高级赛前赔率和滚球赔率分别使用 `football:advanced-prematch-odds` 与 `football:inplay-odds`；历史赔率使用 `football:history:odds`。未开通相应附加包会得到 `403`，三个权限不会互相推导。

请求示例：

```bash
curl --fail-with-body \
  'https://goalxdata.com/api/v1/football/pregame-index?date=2026-08-02&mainLinesOnly=true&limit=20' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer <API_KEY>'
```

成功响应的 `data` 是比赛数组。每场包含 `gxf_` 比赛编号、`gxc_` 赛事编号、中英文赛事与球队、北京时间、快照时间、新鲜度、市场、公司简称、指数和赔率；不会返回采集方名称、原始编号、公司全名或内部字段。`bookmakerCount`、`marketCount`、`indexCount` 和 `oddsCount` 可用于核对该场实际返回量。默认不传 `mainLinesOnly`、`maxLinesPerBookmaker` 或公司/市场筛选时，单场响应不主动截断指数和赔率记录。

读取历史赔率出盘与临场：

```bash
curl --fail-with-body \
  'https://goalxdata.com/api/v1/football/matches/gxf_xxx/pre-match-index/opening-closing' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer <API_KEY>'
```

## 4. 调用频率与限流

- 赛前当前赔率允许并建议最快每 `2` 秒读取一次。
- 赛中滚球赔率允许并建议最快每 `2` 秒读取一次。
- 历史赔率按需读取出盘与临场；不需要每 2 秒重复调用。
- 先用列表接口取得 `gxf_`，再只轮询实际关注的单场。
- 每个 API Key 有两个互不占用的额度池：所有非赔率 REST 请求合计每小时 `4,000` 次；所有赔率 REST（赛前当前、滚球、历史赔率）合计另有每小时 `4,000` 次。均不设每秒限制。
- 每个 HTTP 请求只计 `1` 次，响应中返回的公司、指数和赔率记录条数不另计次。
- 响应会返回 `RateLimit-*`；收到 `429` 时必须等待 `Retry-After` 后再请求。
- 收到 `503` 时按响应给出的 `retryAfterSeconds` 重试，默认等待 60 秒，不使用旧数据兜底。

三天试用 Key 也使用上述两个小时额度池，但到期后立即失效；是否包含各赔率附加包以合同/签发记录为准。

## 5. WebSocket 正式商业版

赔率客户先用长期 API Key 调用 `POST /api/v1/realtime/ticket`，提交 `fixtureIds`（每条连接 1–100 场）、可选的 `channels`（`prematch`、`inplay`）和断线续传用的 `afterSequence`。成功后使用响应中的短期 `wss://stream.goalxdata.com/v1/odds?ticket=...` 地址连接。高级赛前赔率与滚球赔率附加包分别授权 `prematch` 和 `inplay`。

大白话流程只有四步：

1. 客户先从比赛列表或赛前赔率接口取得 `gxf_` 比赛编号。
2. 客户自己的后端拿长期 API Key 换一张 90 秒有效、只能连接一次的临时票。
3. 客户用返回的 `data.url` 建立 WebSocket；连接后 GoalXData 主动推送，不需要反复请求。
4. 客户保存最大的字符串 `sequence`。断线时用长期 Key 换新票，并把该值放进 `afterSequence`；旧票永远不能重用。

长期 API Key 只能保存在客户后端，不能放进网页、浏览器扩展、手机 App 或前端 JavaScript。浏览器如需直连，只能从客户自己的后端取得短期 `data.url`。API Key、短期 URL 和其中的 ticket 都不得写入日志。

- 创建票据算普通 REST 额度 `1` 次；WebSocket 推送消息不计 REST 次数。
- 连接票据有效期为 `90` 秒；只限制开始连接的时间，已经建立的连接不会因票据到期而主动断开。
- 每个 API Key 最多同时 `2` 条连接，心跳间隔 `20` 秒。`24` 小时是状态恢复窗口，不是完整事件回放承诺。
- 客户必须保存最大 `sequence`，按 `sequence` 或 `eventId` 去重；重连时把最大值作为 `afterSequence` 创建新票据。
- 匹配订阅的缺口不超过 `2,000` 个事件时提供无缺口增量续传。
- `resume_reset` 表示游标已超出状态恢复窗口、超过 `2,000` 事件或游标无效；此时 `replayComplete=false`，客户应接受紧随其后的最新全量快照并从新水位继续，中间事件不会补发。
- 商业服务健康状态可通过 `GET https://stream.goalxdata.com/health` 读取；该地址不返回客户、序号或内部水位。

服务端消息顺序和处理规则：

- `subscribed`：订阅已经接受，核对 `channels` 和 `fixtureIds`。
- `odds`：`delivery` 为 `initial`、`replay` 或 `live`。`payload` 是该比赛、该阶段的最新完整赔率状态，不是只修改一个字段的小补丁；应按 `fixtureId + stage` 原子替换本地状态。盘口指数统一读取 `index`；`lineName` 只是暂时保留的兼容旧别名。公司统一读取 `bookmakerCode`；兼容字段 `bookmakerName` 也只会返回同一个简称，不会返回公司全名。
- `resume_reset`：立即清空旧的本地赔率状态，进入重建模式；接受随后所有 `delivery=initial` 的最新快照，直到 `ready`。
- `ready`：初始化、续传或状态重建完成；保存 `lastSequence` 与已收到的最大 `sequence` 中较大者。
- `pong`：客户发送 `{"type":"ping"}` 后的应用层响应。服务端同时使用标准 WebSocket ping/pong 检查死连接。
- `error`：只表示客户消息格式不支持或 JSON 无效；修正客户端，不要原样高频重试。

握手阶段可能返回：`401`（票据缺失、无效、过期或已使用）、`403`（权益或比赛已失效）、`429`（同一 Key 已有 2 条连接）、`503`（服务停止中或票据存储暂不可用）。已经连接后，常见关闭码为 `1001/server_restart`、`1011/initialization_failed` 和 `1013/slow_consumer`。这些情况都必须重新换票；`1013` 还应先提高消费速度或降低单连接订阅量。

可直接运行的客户示例：

- [Node.js WebSocket 示例](/examples/goalxdata-websocket-node.mjs)（先运行 `npm install ws`）
- [Python WebSocket 示例](/examples/goalxdata-websocket-python.py)（先运行 `pip install httpx websockets`）

两个示例都要求显式设置三个环境变量：`GOALXDATA_API_KEY`、逗号分隔的 `GOALXDATA_FIXTURE_IDS`，以及 `GOALXDATA_CHANNELS`。只买高级赛前赔率时填 `prematch`；只买滚球赔率时填 `inplay`；两个包都有时填 `prematch,inplay`。频道必须与合同权益一致，否则换票接口会返回 403。长期 API Key 不要写进源码、命令行参数或日志。

示例会把断点绑定到 API Key 的不可逆指纹、比赛集合和频道集合；任一项变化都会自动清空旧游标并重新取得当前快照。HTTP 400/401/403 和 WS 1008 会停止重试并要求修正合同或配置；429 遵守 `Retry-After`；临时 5xx/1001/1011/1006 才退避重连。若收到 1013 慢消费者关闭码，示例会停止，客户应减少单连接比赛数或把比赛拆到最多两条连接后再启动，不能原配置死循环。

## 6. 数据读取

除高级赔率外，足球 API 继续提供：

- 赛事、赛季、球队和球员目录
- 当前赛季比赛列表和单场详情
- 35 个比赛模块
- 积分榜、阵容、球队赛季指标和球员赛季指标
- 赛后新闻
- 获授权的历史比赛与历史详情

完整机器合同见 [OpenAPI](/openapi/goalxdata-football-v1.json)。
