业务 API 总览
拿到 access token 之后能调什么:api.rankhub.cc 的路径族、Bearer 鉴权与 scope 判定、v1 envelope、游戏面的能力矩阵、分页与限流。
拿到 access_token 之后,业务接口全部走 https://api.rankhub.cc。
本页讲清五件事:路径怎么分、凭据怎么带、scope 怎么判、返回体是什么形状、哪些形状你的应用能调。
游戏数据只有一个面:/v1/games/**
OAuth 应用的游戏数据只经 /v1 访问,全部在 /v1/games/**。
旧的游戏 / 社交接口(/v0/games/**、/v0/social/**)本期只服务 RankHub 自有客户端,
第三方令牌打这些路径会拿到 403 game.firstPartyOnly —— 只有曲目别名与谱面评论
两族是过渡例外(见下方路径族)。目录里的路径都按实际可用标注。
路径族
| 用途 | 前缀 | 响应形状 |
|---|---|---|
| OAuth / OIDC 协议面 | https://auth.rankhub.cc/v1/{oauth,oidc,auth,catalog} | 协议原生(例外) |
| 当前用户资料 | /v1/me/** | v1 envelope |
| 游戏数据(档案 / 成绩 / 最佳成绩 / 删除申请) | /v1/games/** | v1 envelope |
| 游戏任务(票据建档 / 导入 / 同步) | /v1/games/**、/v1/tasks/** | v1 envelope,提交是 202 |
| 开发者面(OAuth 应用管理) | /v1/developer/** | v1 envelope |
| 旧的游戏 / 社交面 | /v0/games/**、/v0/social/** | 无 envelope(仅第一方) |
| 过渡例外:曲目别名、谱面评论 | /v0/games/**/song-aliases/**、/v0/social/** | 无 envelope(第三方仍可用) |
两套前缀的区别
/v1:统一的{ code, message, description, data, requestId }envelope,字段camelCase。/v0:直接返回对象/数组(没有 envelope),字段snake_case。
/v0 上的游戏数据接口只剩第一方与两个过渡例外;不要把 /v0 的档案/成绩接口当成能给第三方用。
先问服务端:能力矩阵
同一个 /v1/games/** 前缀下,不是每个游戏都支持写入。第一步应该拉一次能力矩阵,
而不是把你需要的游戏写死:
curl -sS https://api.rankhub.cc/v1/games \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"code": "common.ok",
"data": {
"games": [
{ "game": "maimai", "read": true, "pushProfile": true, "pushScores": true, "deletionRequest": true },
{ "game": "phigros", "read": true, "pushProfile": true, "pushScores": true, "deletionRequest": true }
]
}
}| 字段 | 含义 |
|---|---|
read | 可读档案、成绩与最佳成绩 |
pushProfile | 可推档案建号(POST /v1/games/{game}/profiles) |
pushScores | 可推成绩(POST /v1/games/{game}/profiles/{profileId}/scores) |
deletionRequest | 可提交删除申请 |
四列对 10 个游戏全部成立(全部为 true):
- 建档 = 直接填字段:调用方自己拿到玩家在该游戏里的身份与资料,把字段填进
player(逐游戏的身份字段表见游戏档案)。服务端只落库, 不发起任何上游请求、不保存任何凭据 —— 所以档案建出来不带同步凭据 (默认syncCapable: false;若用户此前已绑过该游戏的账号级凭据则可能直接是true, 一律读字段为准),之后靠你推成绩更新。 - 推成绩 = 直接填评分值:单曲评分值由你在
scores[].rating里给(各游戏含义不同), 服务端不代算,只做上界校验与"只升不降"合并。maimai/arcaea是例外: 这两族服务端自己算分,rating字段对它们无效。 read/deletionRequest同样对所有游戏成立。
也就是说:第三方不需要任何"材料 / 凭据 / 上游拉取",建档与推成绩都是纯字段写入;
需要登录、验证码、二维码的那些流程属于第一方(POST .../profiles/create 等),
对你的应用仍然是 403 game.firstPartyOnly。
能力矩阵以服务端为准:将来放开时响应会变,路径与形状不变 ——
所以别把它抄成本地常量,客户端要按 false 优雅降级(把对应入口藏起来)。
第三方 vs 第一方:哪些形状能调
| 形状 | 第三方应用 | 说明 |
|---|---|---|
GET /v1/games | ✅ | 能力矩阵 |
GET /v1/games/{game}/profiles、.../{profileId} | ✅ game:read | 授权用户本人的档案 |
GET .../{profileId}/scores、.../{profileId}/bests | ✅ game:read | 成绩与最佳成绩 |
POST /v1/games/{game}/profiles | ✅ game:write | 推档案建号(10 个游戏,填身份字段),标注来源 |
POST .../{profileId}/scores | ✅ game:write | 推成绩(10 个游戏,填评分值;只升不降) |
POST .../{profileId}/deletion-requests | ✅ game:write | 申请删除,由用户确认 |
GET .../deletion-requests/{requestId} | ✅ game:write | 查你自己发起的那条申请 |
直接 DELETE 档案 | ⛔ 403 game.deletionConsentRequired | 删除必须用户点头 |
POST .../deletion-requests/{requestId}/decision | ⛔ 403 game.firstPartyOnly | 用户在 RankHub 里表态 |
POST .../profiles/create(票据建档)、.../profiles/import | ⛔ 403 game.firstPartyOnly | 需要用户的游戏平台凭据 |
POST .../{profileId}/sync | ⛔ 403 game.firstPartyOnly | 同步由 RankHub 执行 |
/v1/tasks/** | ⛔ 403 game.firstPartyOnly | 任务面只属于第一方 |
/v1/games/maimai/relay/** | ⛔ 403 game.firstPartyOnly | maimai 机台中转只给 App |
/v0/games/**、/v0/social/** | ⛔ 403 game.firstPartyOnly | 见上文的过渡例外 |
一句话:你能"推数据",不能"代替用户同步",也不能"代替用户删"。 被拒绝时不是"重试就能过",是形状本身不开给你 —— 换用上表 ✅ 的那条路径。
认证
第三方应用用 OAuth 拿到的 access token(at_ 开头的不透明串)走标准 Bearer:
curl -sS https://api.rankhub.cc/v1/games/maimai/profiles \
-H "Authorization: Bearer $ACCESS_TOKEN"- access token 有效 600 秒,过期前用
refresh_token换新(见令牌、刷新与撤销)。 - access token 不是 JWT,没有可本地解析的 claim;要时长就读令牌响应的
expires_in。 - 浏览器会话(Cookie)也能调这些接口,但第三方应用不应该、也无法拿到 RankHub 的会话 Cookie。 会话与内置 App 属于第一方,能调全部形状;第三方令牌只能调上表 ✅ 的那些。
scope 如何判定
业务 API 会按路径域检查 scope,判定点是「凭证是 OAuth access token 时」:
| 路径 | GET 需要 | 非 GET 需要 |
|---|---|---|
/v1/games/** | game:read | game:write |
/v0/games/**(第一方 / 过渡例外) | game:read | game:write |
/v0/social/**(第一方 / 过渡例外) | social:read | social:write |
/v0/auth/me/** 等资料面 | profile:read | profile:write |
| 会话 / 密码 / MFA 等敏感面 | account_security:read | account_security:write(部分只允许浏览器会话) |
/v1/developer/** | development:read | development:write |
三条补充规则:
- 写覆盖读:持有
game:write的令牌也能做game:read的请求。 - scope 够不等于形状够:
game:write不会让你越过「仅第一方」的边界(那是403 game.firstPartyOnly,补 scope 也没用)。两层判定是分开的。 - 公开读(可选登录)不做 scope 判定:这类接口带有效凭据就按该用户返回个性化字段, 不带就匿名;只有必须登录的接口才会因为缺 scope 返回 403。
缺 scope 时的 403 文案是固定的:
{
"statusCode": 403,
"message": "OIDC access token requires game:write",
"error": "Forbidden"
}(上面是 /v0 的形状。/v1 面上同类失败走 v1 envelope,code 见错误与排障。)
只给必要 scope
game:write 等同「代表用户改游戏数据」,social:write 等同「代表用户发内容」。
注册应用时就把用不到的写 scope 去掉(scope 在授权页上会原样展示给用户)。
v1 的响应 envelope
/v1/** 统一:
{
"code": "common.ok",
"message": "",
"description": null,
"data": { },
"requestId": "req_01J8Z4K3Y8W2M0B7V6N5C4X3A2"
}| 字段 | 说明 |
|---|---|
code | 稳定业务码(common.* / auth.* / game.* / task.*)。按它分支,不要按 message |
message | 面向人的短句(可能为空) |
description | 更长的补充说明,可能为 null |
data | 业务对象;失败时通常为 null(唯一例外见 428) |
requestId | 与响应头 X-Request-Id 相同,排障时带上它 |
补充:
- 业务码目录可从
GET https://auth.rankhub.cc/v1/catalog/codes拉取(公开只读),用于把码翻成人话。 - 异步任务提交成功是
202+code: "common.ok"。 - 删除申请提交成功是
202+code: "common.accepted"(这是common.accepted目前唯一的用法)。 - 失败的响应里
data通常为null;只有428 game.writeConfirmationRequired例外, 它必须带机读的data.confirmation(见成绩读取与写入的确认重试)。 - 时间字段是 RFC3339(可能带纳秒),字段名 camelCase。
v0 的响应形状
/v0 的游戏 / 社交接口直接返回对象/数组(没有 envelope),字段是 snake_case:
{
"id": "1c2f...",
"player": { "name": "Player", "rating": 15000 },
"is_public": true,
"created_at": "2026-10-01T12:00:00.000Z",
"updated_at": "2026-10-20T08:30:00.000Z"
}失败时通常是这样(没有 envelope):
{
"statusCode": 400,
"message": "scores must be a non-empty array",
"error": "Bad Request"
}message 可能是字符串,也可能是字符串数组(校验失败时逐字段列出)。
请以 statusCode + 业务语义分支,不要匹配文案。
本期第三方只会在过渡例外里见到 /v0 形状
除曲目别名与谱面评论外,第三方令牌打 /v0 游戏 / 社交接口一律 403 game.firstPartyOnly。
新接入请直接按 /v1/games/** 设计,不要为 /v0 写适配层。
分页与批量上限
列表一律游标分页,形状 { items, nextCursor, hasMore }:
| 参数 | 说明 |
|---|---|
limit | 页大小,最大 100(默认 50) |
cursor | 上一页返回的 nextCursor,首页不传;对客户端不透明,不要自己拼 |
nextCursor 为 null / hasMore 为 false 就到底了。排序参数一换,旧游标就不再适用 ——
换了 sortBy / order / difficulty 请从第一页重来,不要复用。
排序只在有明确参数的列表上支持:成绩列表用 sortBy(playTime / achievements / rating)
order(asc/desc);其余列表按各接口文档里的默认顺序,不要假设一个稳定的全能排序。
批量写入的两条硬限制:
| 限制 | 值 | 超出时 |
|---|---|---|
| 单批条数 | 默认 500,服务端可调 | 400 common.invalidArgument(这是参数错误,不是限流) |
| 请求体大小 | 1 MiB | 413 |
单批条数不是用来当"一次传完整个曲库"的:分批提交、每批留出重试余量,比调大单批更稳。
限流与 429
平台按应用做限流(只对第三方生效;第一方 App 与浏览器会话豁免,避免误伤自家客户端)。
- 命中限流 ⇒
429+code: "common.rateLimited"+Retry-After(秒); - 请按
Retry-After退避重试(带抖动),不要立刻重试同一批; - 单批条数超限是
400,不是429—— 两者含义不同,别混着处理; - 限流是削峰手段,不是鉴权:它不会因为你换了令牌而重置,按应用算。
常见状态码
| 状态码 | 含义 | 处理 |
|---|---|---|
200 / 201 | 成功(读 / 建号) | — |
202 | 已接受(异步任务、删除申请) | 看 code 区分:common.ok = 任务,common.accepted = 删除申请 |
401 | 没有凭据 / 令牌失效 / 会话被撤销 | 刷新令牌后重试一次;仍失败则重新授权 |
403 | 缺 scope,或该形状只给第一方,或写入被暂停,或删除要用户同意 | 读业务码:补 scope 需重新授权;其余三种不要重试 |
404 | 档案/曲目/评论不存在,或不属于当前用户 | 不要重试;检查 profileId 与账号 |
409 | 冲突(重复的活跃提案、幂等键参数不一致、确认令牌失效等) | 读 message/业务码,换参数或复用已有资源 |
413 | 请求体过大(上限 1 MiB) | 分批提交 |
422 | 语义不可处理(如不支持的任务组合、越界的成绩取值) | 不要重试 |
428 | 写入需要用户确认 | 带上 data.confirmation.confirmationToken 原样重放同一份 payload |
429 | 触发限流 | 按 Retry-After 退避重试(带抖动) |
503 | 鉴权或数据服务暂不可用 | 退避重试;持续出现请联系平台 |
四个游戏面专属业务码(game.firstPartyOnly / game.writeConfirmationRequired /
game.writeSuspended / game.deletionConsentRequired)的出现场景与处理见错误与排障。