RankHub
业务 API

业务 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.firstPartyOnlymaimai 机台中转只给 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:readgame:write
/v0/games/**(第一方 / 过渡例外)game:readgame:write
/v0/social/**(第一方 / 过渡例外)social:readsocial:write
/v0/auth/me/** 等资料面profile:readprofile:write
会话 / 密码 / MFA 等敏感面account_security:readaccount_security:write(部分只允许浏览器会话)
/v1/developer/**development:readdevelopment:write

三条补充规则:

  1. 写覆盖读:持有 game:write 的令牌也能做 game:read 的请求。
  2. scope 够不等于形状够:game:write 不会让你越过「仅第一方」的边界(那是 403 game.firstPartyOnly,补 scope 也没用)。两层判定是分开的。
  3. 公开读(可选登录)不做 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 MiB413

单批条数不是用来当"一次传完整个曲库"的:分批提交、每批留出重试余量,比调大单批更稳。

限流与 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)的出现场景与处理见错误与排障。

下一步

本页内容