API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)
写入格式(建档 / 推成绩)
第三方写入到底要传什么:POST profiles 与 POST scores 的请求体结构、共享字段表、每个游戏的必填字段与单位、成功响应、校验与 400/422/428/403。
本页是写入侧的统一契约。读侧(返回什么)见游戏数据格式总览; 每个游戏的具体字段表与可直接抄的 JSON见各游戏格式页的「写入(第三方)」一节。
两个写入端点:
| 动作 | 端点 | 说明 |
|---|---|---|
| 建档 | POST /v1/games/{game}/profiles | 填身份字段建一张档案;可选地同时带一批成绩 |
| 推成绩 | POST /v1/games/{game}/profiles/{profileId}/scores | 往已有档案批量写成绩 |
两者都是 application/json、Bearer 鉴权、v1 envelope;写操作需要 game:write(覆盖 game:read)。
交互式 schema 在 API Reference
每个写入端点的完整 JSON Schema(含所有可选字段与校验标记)见 API Reference 与 建档端点,可直接在页面上试调。
写入代表用户修改其 RankHub 档案
只提交用户在你这一侧真实产生的成绩;不要伪造、不要批量灌库。所有写入进成绩历史, 可能触发评分重算与通知;批量或连续异常会暂停该应用的写权限。
不确定该传 RankHub 字段还是游戏原生数据?先看怎么传数据(速查)。
1. 建档请求体
{
"player": { "name": "Player", "friendCode": "1234567890123" },
"idempotencyKey": "b4c0f0d6-8f6f-4d6a-a0d3-3c6f0c4a11"
}| 顶层字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
player | object | 支持 | 玩家字段,见下 |
scores | array | 可选 | 建档时一并写入的成绩(与推成绩的 scores[] 同结构),受单批上限约束。⚠️ chunithm / orzmic 会静默忽略,这两个游戏建档后需另行 POST .../scores |
idempotencyKey | string ≤128 | 可选 | 幂等键,作用域 = 当前用户 + 当前应用;重复提交返回同一张档案 |
1.1 player 是超集:字段按游戏生效
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string ≤64 | 支持 | 展示名(10 个游戏都要) |
rating | number | 可选 | 只是提示:服务端会按 scores 重算(没有成绩则 0),返回的 player.rating 以服务端存量为准 |
| 身份字段 | 见下表 | 支持 | 每个游戏不同,缺失/不合法 ⇒ 400 |
| 其它原生字段 | 见各游戏页 | 可选 | 不适用的字段被忽略(不会报错) |
身份字段(10 个游戏):
| 游戏 | 身份字段 | 备注 |
|---|---|---|
maimai | friendCode 或 userId | 二者至少一个;另有 courseRank/classRank/star 可选 |
arcaea | arcaeaId + character | ❗ character 缺失 ⇒ 400;arcaeaId 缺失不报错(落 0,身份就错了) |
phigros | objectId | 可选 shortId |
milthm | milthmUserId + region | region 取 cn/global,缺省 cn |
notanote | notanoteUid | 数字串按字符串传,避免前导零/大整数失真 |
musedash | musedashUid | 1~32 位 [0-9A-Za-z_-](云存档解析器另要求 32 位 hex) |
rizline | playerId | |
osu | osuUserId | bancho 数字 id,按字符串传 |
chunithm | friendCode | 街机好友码,不是 LXNS id |
orzmic | externalId | 本地游戏无官方账号体系,值由你与用户约定 |
建档只写
player(身份字段)与可选scores;建档后的档案syncCapable: false, 之后靠你推成绩更新。
2. 推成绩请求体
{
"scores": [
{
"songId": "1234",
"difficulty": "IN",
"score": 1000000,
"accuracy": 100,
"rating": 15.2,
"playTime": "2026-10-20T12:00:00Z"
}
],
"idempotencyKey": "0b6f0f1e-2f3a-4c5d-8e9f-1a2b3c4d5e6f",
"confirmationToken": "(仅 428 重放时带)"
}| 顶层字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
scores | array | 支持 | 1~单批上限条(默认 500,硬上限 2000),见 §5 |
idempotencyKey | string ≤128 | 可选 | 同批重放去重;不传也不会重复计分("只升不降"本身保证重放无害) |
confirmationToken | string ≤256 | 仅 428 后 | 用户确认后原样重放时带,见 §6 |
2.1 scores[] 共享字段
字段是超集:按游戏生效,不适用的字段被忽略。每个游戏"哪几个必须给"见各游戏页的速查表。
| 字段 | 类型 | 含义 | 谁用 |
|---|---|---|---|
songId | string | number | 曲目 id(osu 是 beatmap id);arcaea 守卫开启时必须是字符串 | 全部(必填) |
type | string | 谱面类型 standard/dx/utage,缺省 dx | maimai |
difficulty | string | number | 难度标识(各游戏自己的集合;osu 是模式;maimai 不用,用 levelIndex) | 除 maimai |
levelIndex | int | 难度下标 0~4 | maimai |
level | string | 难度标签(展示用) | 可选 |
achievements | number | 达成率(maimai 0~100.5/101;多数游戏作为 accuracy 的兜底;osu 的 achievements ≤1 时按 0~1 乘 100) | maimai;其余可作 accuracy 兜底 |
score | number | 原始得分 | arcaea 0~10,010,000(大 Pure 可略超 1,000 万);phigros/milthm/musedash/rizline 0~1,000,000;osu total_score;本地游戏比较值 |
rating | number | 单曲评分:phigros rks / milthm reality / notanote nrk / musedash RL / rizline rks / osu pp | 6 个游戏;maimai/arcaea 忽略(服务端算) |
accuracy | number | 准确率 0~100 百分数(osu 不做 0~1 换算,传 0.98 会被当成 0.98%) | phigros/milthm/notanote/musedash/rizline/osu |
constant | number | 谱面定数 0~100 | phigros/milthm/notanote/rizline/musedash |
combo | int | 连击数 | musedash/notanote/rizline/osu |
cleared | bool | 是否通关(缺省 false;osu 的 passed 取 cleared !== false) | 本地/rizline/notanote/musedash/osu |
fullCombo | bool | 是否全连 | rizline/musedash/phigros(也认 fc) |
fc / fs | string | 全连 / 全同步档位 | maimai;phigros/rizline 用 fc 的存在性表示全连 |
dxScore / dxStar | int | DX 分数 / 星级(缺省 0) | maimai |
evaluate | int | 结算评级档(0=D 1=C …) | musedash |
rank | string | 评级文本(SH/XH/S…);osu 缺省 F | osu / phigros / maimai 等 |
mode | string | 模式 osu/taiko/fruits/mania(❗ 不是 catch) | osu |
upstreamId | string | 该行在上游榜单里的成绩 id | osu |
position | int | 榜内位次 | osu |
playTime | string | RFC3339;缺省用服务端时间 | 目前仅 osu 读取;其它游戏传了不生效 |
chartId | string | 谱面 id(缺省用 songId) | milthm |
chartKey / color / native | string | 谱面键 / 轨道颜色 / 来源(common/fanmade,其它值归 common) | notanote |
trackId | string | 谱面 id(缺省用 songId) | rizline |
uid | string | songUid(缺省用 songId) | musedash |
artist | string | 曲师(展示用) | phigros/milthm/rizline |
realityFormula | string | v3 = Reality V3 公式,其它按 v2 | milthm |
maxHit / rizHit | int | 最大击打 / Riz 击打 | rizline |
bucket | string | 落到哪个桶(chunithm best30/selection10/new20,orzmic best30;缺省 best30) | 本地游戏 |
native | object | 原始成绩对象(原样存、原样用) | chunithm / orzmic |
scoreBelowMax / shinyPerfectCount / perfectCount / nearCount / missCount / health / clearType / bestClearType | int | Arcaea 判定计数与通关类型(缺省 0) | arcaea |
缺省值说明:计数类字段"缺省 0"是有意的诚实默认(未知就是 0),不是服务端帮你补全。 需要准确分数时请把真实值给全。
2.2 原生数据(native)
手里已经是游戏原生对象时,整块放进 native 即可(建档 player.native、成绩 scores[].native),
服务端按该游戏唯一一套换算规则归一;native 优先于顶层同名字段,不合法 ⇒ 400 common.invalidArgument。
- 哪些游戏支持 native 建档 / native 成绩、每个游戏必给什么:见怎么传数据(速查);
- 每游戏的 native 字段表:见对应游戏格式页;
- 机器可读版本:
GET /v1/games/{game}/format的profile.nativeFields/score.nativeFields。
3. 响应
3.1 建档 → 一张档案
data 与 GET /v1/games/{game}/profiles/{profileId} 完全相同(见总览 §2)。
第三方建的档案会带 origin: "app"、createdByClientId、syncCapable: false。
3.2 推成绩 → 写入结果
{
"code": "common.ok",
"data": {
"rating": { "before": { "total": 15100 }, "after": { "total": 15234 }, "delta": 134 },
"applied": [ { "songId": "1234", "improved": true, "currentAchievements": 100.5125 } ],
"ignored": [ { "songId": "5678", "reason": "notImproved" } ]
}
}
rating.before/after的键集合随游戏:maimai 是total/latest/archive, arcaea 是potential/b30Avg/b50Avg(没有total,delta是 potential 差), 其余多为total。applied[].currentAchievements对 arcaea 装的是分数score、 本地游戏(chunithm/orzmic)恒为null;ignored[].reason本地游戏是not-improved(带连字符)。
| 字段 | 说明 |
|---|---|
rating.before / after | 本次写入前后该游戏的"总力"(键集合随游戏,至少 total) |
rating.delta | after.total - before.total;无 total 概念时为 null |
applied[] | 处理过的成绩:songId / improved(是否抬高)/ currentAchievements(写入后达成率,无此概念为 null) |
ignored[] | 被忽略的提交:reason 如 notImproved(没变好)、unknownChart(谱面不存在) |
一条成绩可能同时出现在
applied(improved: false)与ignored里,便于两条流都渲染。
4. 常见 400 文案
| 场景 | 返回 |
|---|---|
| 该游戏不支持这种写形状 | 400 common.invalidArgument,description = <game> does not support … |
phigros difficulty 缺失/非法 | 400,Phigros score difficulty is required / … is invalid |
身份字段缺失(如 musedashUid) | 400,文案随游戏(musedashUid/notanoteUid … is required for … profile creation、objectId is required、chunithm profile creation requires player.friendCode);❗ arcaea 缺 arcaeaId 不报错(落 0) |
| maimai 两个身份都没给 | 400,Either user_id or friend_code is required |
arcaea 缺 character | 400,character is required |
| 单批超限 | 400 common.invalidArgument |
| 数值越界 / 谱面不存在 / 组合矛盾 | 400(非法值)或 422 common.unprocessable(语义矛盾) |
通用上界受写入守卫开关影响
上面的"越界 400/422"分两类:
- 各游戏服务自身的校验(如 phigros 的
difficulty、rizline 的score、身份字段)——总是生效; - 通用/谱面级上界(数值范围、谱面存在性、组合自洽)挂在写入守卫上,
writeGuardMode默认off时不拦。不要把"没报错"当成"数值一定合法"。
5. 批量与限流
scores不能为空(ArrayNotEmpty);单批上限按应用档位配置,默认 500,DTO 硬上限 2000;- 超限 →
400 common.invalidArgument,不会部分写入; - 批量写入限流按 tier(见业务 API 总览)。
6. 428 确认与 403 停写
写入守卫会对可疑写入返回 428 game.writeConfirmationRequired(需要用户确认后原样重放)或
403 game.writeSuspended(应用写权限被暂停)。两种语义、payload 与重放规则见
成绩读取与写入 · 428 / 403。
7. 幂等
| 端点 | 幂等键 | 语义 |
|---|---|---|
| 建档 | idempotencyKey | 作用域 = 用户 + 应用;重复提交返回同一张档案(不会重复建号) |
| 推成绩 | idempotencyKey | 只用于重试去重;不传也安全("只升不降"保证重放不重复计分) |