RankHub

API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)

成绩读取与写入

在 /v1/games/** 上读成绩与统一形状的最佳成绩;10 个游戏都能推成绩(评分值由调用方给),只升不降、单批上限、428 用户确认与 403 停写。

成绩数据分两条路:

  • 读:GET /v1/games/{game}/profiles/{profileId}/scores 与 GET .../bests(最佳成绩,所有游戏一个形状)。
  • 写:10 个游戏都支持批量写入。单曲评分值由你给;maimai / arcaea 例外(服务端自己算分)。

写的是「用户数据」

批量写入代表用户修改其 RankHub 档案。只提交用户在你这一侧真实产生的成绩; 不要伪造、不要批量灌库。所有写入都会进入成绩历史,并可能触发评分重算与通知。 所有游戏都有通用上界校验(越界直接 400/422);maimai / arcaea 另有标定分类, 低频异常可能要求用户确认后重试,批量或连续的异常会暂停该应用的写权限。

读取成绩

所有游戏一条路径,字段跨游戏归一(游戏原生字段在 native):

curl -sS "https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/scores?limit=100&sortBy=rating&order=desc" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
查询参数说明
limit页大小,最大 100(默认 50)
cursor上一页的 nextCursor,对客户端不透明
sortByplayTime / achievements / rating(跨游戏归一的三键)
orderasc / desc(默认 desc)
difficulty按各游戏原生难度标识过滤(maimai 的 3、arcaea 的 2…)

不传 sortBy 时按各游戏的默认顺序(与旧接口一致)。响应是 { items, nextCursor, hasMore }:

{
  "code": "common.ok",
  "data": {
    "items": [
      {
        "scoreId": "b1f0c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "songId": 1234,
        "songName": "Example Song",
        "type": "dx",
        "difficulty": null,
        "levelIndex": 3,
        "level": "13+",
        "achievements": 100.5125,
        "score": null,
        "rating": 320,
        "rank": "sss",
        "dxScore": 2500,
        "dxStar": 5,
        "fc": "ap",
        "fs": "fs",
        "playTime": "2026-10-20T12:00:00Z",
        "native": { "dx_rating": 320 }
      }
    ],
    "nextCursor": null,
    "hasMore": false
  }
}
字段说明
songId曲目 ID(maimai 是数字串;osu 是 beatmap id)
type / difficulty / levelIndex / level谱面标识;difficulty 的取值集合随游戏(osu 是模式名)
achievements / score达成率/准确率 与 原始得分,按游戏填其中一个(另一个 null,量纲随游戏)
rating单曲 rating / potential / rks / pp(服务端算或调用方给,见下)
native游戏原生成绩对象(未归一,仅供展示/调试)

完整字段表(含 maimai 的 dxScore/dxStar/fc/fs、rank、playTime)见 游戏数据格式总览;每个游戏 score.native 里有什么见各游戏格式页。

null 表示「这个游戏没有这个概念」

type 只有 maimai 有、difficulty 只有 arcaea 等游戏有。请按 null 渲染占位, 不要当 0 或空字符串处理。跨游戏渲染请只用归一字段,游戏特异的地方读 native。

最佳成绩:一个统一形状

旧接口按游戏分成 best50 / best30 / b40 / best100 / best, 现在是同一个端点、同一个形状:

curl -sS https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/bests \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "code": "common.ok",
  "data": {
    "game": "maimai",
    "computedAt": "2026-10-21T18:40:00Z",
    "totals": { "total": 15234, "latest": 7234, "archive": 8000 },
    "buckets": [
      { "key": "latest",  "label": "当期", "total": 7234, "items": [ /* 与 scores 同形状 */ ] },
      { "key": "archive", "label": "往期", "total": 8000, "items": [] }
    ]
  }
}
字段说明
game游戏键
computedAt本次计算时间。曲库更新后历史档案的展示值会变,用它判断新鲜度
totals至少含 total(该游戏的"总力");各游戏可另加键(maimai 的 latest / archive)
buckets[].key稳定的桶键,客户端据此选标签 / 顺序,不要按数组位置猜
buckets[].label展示名(zh-CN,仅建议;你可以自行本地化)
buckets[].total该桶合计;该游戏无此概念时为 null
buckets[].items桶内成绩,形状与 scores 的 items 完全一致

不要自己算分

分桶口径与算法在服务端(maimai 是「当期 15 / 往期 35」,按谱面所属版本分桶, 不是按 standard / dx 谱面类型)。客户端自己算出来的数字一旦与平台不一致,用户会先怀疑你。 桶键是稳定的,桶的数量与含义随游戏不同 —— 渲染前先看 key,不要断言"一定是两个桶"。

每个游戏的桶键、成员规则与 totals 语义,见游戏格式各页的「最佳成绩桶」, 或 GET /v1/games/{game}/format 的 score.buckets。

推成绩(批量写入)

10 个游戏都可用(能力矩阵 pushScores 全为真)。一条路径、一套形状; 差异只在"评分值从哪来"——见下面的评分口径。

要抄请求体?看写入格式页

本页讲接口语义(只升不降、428 确认、停写、限流)。player 超集、scores[] 共享字段、 每个游戏必须给哪几个字段与可直接抄的 JSON,见写入格式(建档 / 推成绩); 单游戏细节见各游戏格式页的「写入(第三方)」。

curl -sS -X POST https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/scores \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "scores": [
      {
        "songId": 1234,
        "type": "dx",
        "levelIndex": 3,
        "achievements": 100.5125,
        "dxScore": 2500,
        "dxStar": 5,
        "fc": "ap",
        "fs": "fs",
        "playTime": "2026-10-20T12:00:00Z"
      }
    ]
  }'
# phigros:单曲评分值(rks)由你给 —— 服务端不代算
curl -sS -X POST https://api.rankhub.cc/v1/games/phigros/profiles/$PROFILE_ID/scores \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "scores": [
      { "songId": "…", "difficulty": "IN", "score": 1000000, "accuracy": 100,
        "rating": 15.2, "playTime": "2026-10-20T12:00:00Z" }
    ]
  }'
# 本地游戏(chunithm / orzmic):native 原样带上,bucket 指定落到哪个桶
curl -sS -X POST https://api.rankhub.cc/v1/games/chunithm/profiles/$PROFILE_ID/scores \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "scores": [
      { "songId": "…", "bucket": "selection10", "native": { "…": "上游算好的成绩对象" },
        "playTime": "2026-10-20T12:00:00Z" }
    ]
  }'

单曲评分值:scores[].rating

评分值由调用方给:你在自己一侧已经算出/拿到了玩家这一局的单曲评分,就把它放进 scores[].rating。服务端不代算,只做上界校验与"只升不降"合并。

每个游戏这个字段叫什么(rks / reality / nrk / RL / pp),见 游戏数据格式总览与各游戏格式页的「评分语义」一节。

maimai / arcaea 例外:这两族服务端自己算分,传 rating 也无效,行为与旧接口一致。

本地游戏(chunithm / orzmic)不一样:成绩是结构不透明的原始 JSON —— 用 scores[].native 原样带上,bucket 指定落到哪个最佳成绩桶(缺省 best30, chunithm 还可以 selection10 / new20、orzmic 只有 best30)。

本地游戏的玩家头部不会被改写

chunithm / orzmic 的 player / summary(玩家头部)不会被你推的成绩改写; 推成绩只影响成绩行与对应桶。

字段是超集(按游戏生效,不适用的被忽略):

  • 谱面身份:maimai 是 (songId, type, levelIndex);arcaea 是 (songId, difficulty); 其余游戏用 songId(部分游戏可另给 chartId / uid / trackId / chartKey 覆盖默认拼法);
  • 成绩内容:achievements / score / accuracy 按游戏填;scores[].rating 见上; 各游戏还有自己的可选字段(maimai 的 dxScore / dxStar / fc / fs、 arcaea 的 perfectCount / nearCount 等、milthm 的 realityFormula、 osu 的 mode / position、rizline 的 maxHit ……);
  • playTime:RFC3339;缺省用服务端当前时间(沿用旧语义);
  • idempotencyKey:可选。不传也没关系 —— "只升不降"本身已经让重放无害。

成功是 200:

{
  "code": "common.ok",
  "data": {
    "rating": {
      "before": { "total": 15234 },
      "after": { "total": 15250 },
      "delta": 16
    },
    "applied": [ { "songId": 1234, "improved": true, "currentAchievements": 100.5125 } ],
    "ignored": []
  }
}
  • applied[]:这一批里的每一条都有一条结果;improved: false 的条目同时出现在 ignored[] 里 (reason 是稳定原因,如 notImproved = 只升不降拦下、unknownChart = 谱面不存在), 方便你把它渲染成"被忽略"的那一支;
  • rating:评分变化,delta 是 after.total - before.total(不是单曲成绩)。 只升不降通常让它 ≥ 0,但曲库定数调整后重算也可能让它变小。

写入语义

规则说明
谱面不存在就插入新谱面直接落库
只升不降更差的提交不改当前成绩(进 ignored);当前成绩保持旧值
每次提交写历史无论是否更好都会追加一条成绩历史(时间取 playTime,缺省服务端时间)
评分重算按全量成绩重算 best 桶与总分(maimai / arcaea 自己算分,其余游戏以你给的 rating 为准);有变化才追加一条评分历史
幂等同批重放不改变当前成绩(只升不降 + 按谱面 upsert),网络超时可直接重试;但每次提交都会再写一条历史,业务层重放请自行判重
硬上界谱面必须存在、取值必须在游戏规则范围内、playTime 不能是未来 —— 越界直接 400 common.invalidArgument / 422 common.unprocessable,不进统计
单批条数默认上限 500(服务端可调),超限 400 common.invalidArgument
请求体≤ 1 MiB,超出 413
档案不存在 / 不是本人的404 common.notFound
账号在注销流程中403

可能要用户确认:428 与确认重试

平台的写入守卫会对可疑写入做校验。低频、单次的越界不会直接拒绝,而是回 428 game.writeConfirmationRequired,让你请用户在 RankHub 里确认这次写入。

428 今天只出现在 maimai / arcaea

只有 maimai / arcaea 有标定分类(它们本来就是服务端算分)。其余 8 个游戏只有 通用上界校验:越界直接 400 common.invalidArgument / 422 common.unprocessable, 不会返回 428,也不会因为这个触发停写。

{
  "code": "game.writeConfirmationRequired",
  "message": "该写入需要用户确认后重试",
  "description": null,
  "data": {
    "confirmation": {
      "confirmationToken": "wc_7Q2…",
      "approveUrl": "rankhub://write-confirmation?token=wc_…",
      "expiresAt": "2026-10-22T19:10:00Z"
    }
  },
  "requestId": "req_…"
}

428 是唯一 data 非空的失败响应 —— 确认令牌必须机读,这是刻意的例外。

用户确认后,用同一份 payload 加上 confirmationToken 原样重放:

{
  "scores": [ /* 与上次完全一致 */ ],
  "confirmationToken": "wc_7Q2…"
}
  • 令牌一次性,绑定「应用 + 档案 + payload 摘要」,默认 30 分钟内有效;
  • payload 变了就得重新发起(改一个字段也算变)——你应当重放的是同一批数据, 不是"带上令牌把新数据也一起写";
  • 令牌过期 / 已用 / payload 不匹配 ⇒ 409 common.conflict,需要重新提交(会再走一次判定)。

不要把 428 当 500 或 403 处理

428 不是失败,是"差一个用户动作"。正确做法:把 approveUrl 展示/推送给用户, 等用户点完再重放;不要自动重试同一个请求(会拿到 409),也不要把它当永久拒绝。

写入被暂停:403 game.writeSuspended

如果写入批量、连续或明显异常,平台会暂停该应用的写权限:

  • 返回 403 game.writeSuspended("该应用的写权限已被暂停");
  • 只拦写:读接口、登录、令牌刷新都不受影响;
  • 暂停是应用级的:你的所有用户、所有游戏的写都会一起停(触发只可能来自 maimai / arcaea 的标定分类,但一旦停了,写 10 个游戏都会被拦);
  • 恢复只能联系平台处理,不要靠换令牌 / 换 key / 重试绕过。

开发者平台的应用详情里能看到停写状态与原因(见注册应用与凭据)。 遇到它请停下来看数据来源,而不是加大重试。

限流

按应用限流,命中返回 429 + common.rateLimited + Retry-After(秒),请按该头退避; 单批条数超限是 400。全局限流口径见业务 API 总览。

下一步

本页内容

官方交流群二维码官方交流群问题反馈 · 版本资讯 · 成绩交流群号 1073434398