API 目前处于 Early Preview 阶段, 可能存在诸多不完善与错误之处,欢迎加群交流反馈:加入官方交流群(1073434398)
游戏档案
在 /v1/games/** 上读档案、推档案建号、看只读标记,以及第三方唯一能做的删除动作——提交申请。
一个 RankHub 用户可以绑定多个游戏的档案;每个档案属于一个用户 + 一个游戏档案 ID。
第三方应用读写档案的前提是拿到该用户的 access_token,并且令牌具备 game:read
(或 game:write,写覆盖读)。
档案接口在 /v1/games/** 上
旧文档里的 /v0/games/** 本期只服务 RankHub 自有客户端:第三方令牌打它会得到
403 game.firstPartyOnly。本页所有路径都是 /v1 形状(envelope + camelCase + 游标分页)。
支持的游戏
路径里的 {game} 是小写游戏键,不是控制器名(maimai 的路径是 maimai,不是 maimai-dx)。
| 游戏键 | 读 | 推档案建号 | 推成绩 | 删除申请 |
|---|---|---|---|---|
maimai | 支持 | 支持 | 支持 | 支持 |
arcaea | 支持 | 支持 | 支持 | 支持 |
phigros | 支持 | 支持 | 支持 | 支持 |
milthm | 支持 | 支持 | 支持 | 支持 |
notanote | 支持 | 支持 | 支持 | 支持 |
musedash | 支持 | 支持 | 支持 | 支持 |
rizline | 支持 | 支持 | 支持 | 支持 |
osu | 支持 | 支持 | 支持 | 支持 |
chunithm | 支持 | 支持 | 支持 | 支持 |
orzmic | 支持 | 支持 | 支持 | 支持 |
四列对 10 个游戏全部为真:读、建档、推成绩、申请删除都可用。
- 建档 = 填身份字段:只写
player与可选scores,没有别的材料; - 推成绩 = 填评分值:单曲评分由你在
scores[].rating里给;maimai/arcaea例外, 这两族服务端自己算分。
这张表只是今天的状态,权威是 GET /v1/games 的能力矩阵(见业务 API 总览)。
每个游戏的字段在哪
这张表只回答"能不能访问"。每个游戏的身份字段、统一字段、native 原生字段、最佳成绩桶见
游戏格式;也可以直接读 GET /v1/games/{game}/format(机读版本)。
读取档案
# 能力矩阵:先看这个游戏支不支持写
curl -sS https://api.rankhub.cc/v1/games \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 列表(游标分页,limit 最大 100)
curl -sS "https://api.rankhub.cc/v1/games/maimai/profiles?limit=50" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 单个档案
curl -sS https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"列表只返回该用户在当前游戏下未解绑的档案,形状是
{ items, nextCursor, hasMore }。未绑定 / 不属于当前用户 / 已解绑的档案一律 404 common.notFound
(不区分"不存在"与"不是你的",避免被拿来探测)。
档案对象(各游戏一致的外壳 + 游戏原生字段进 player.native):
{
"profileId": "8d1f4a0e-6c2b-4a1e-9d3f-2b7c5e9a0f11",
"game": "maimai",
"player": {
"name": "Player",
"rating": 15234,
"native": { "userId": 10000001, "friendCode": "1234567890123", "courseRank": 0, "classRank": 0, "star": 12 }
},
"isPublic": true,
"origin": "app",
"createdByClientId": "cli_7Q2…",
"syncCapable": false,
"lastSyncedAt": null,
"createdAt": "2026-10-21T18:40:00Z",
"updatedAt": "2026-10-21T18:40:00Z"
}| 字段 | 说明 |
|---|---|
profileId | 档案 ID(后续所有 /profiles/{profileId}/** 都用它) |
game | 游戏键 |
player.name / player.rating | 归一后的显示名与该游戏"总分";没有总分概念时为 null |
player.native | 游戏原生玩家对象(未归一,只用于展示/调试) |
origin / createdByClientId | 建档来源(user / app)与第三方应用公开 id |
syncCapable / lastSyncedAt | 只读标记:自动更新开关、最近一次更新时间 |
isPublic / createdAt / updatedAt | 公开性、创建与更新时间(RFC3339) |
完整字段语义见游戏数据格式总览; 每个游戏
player.native里有什么,见各游戏格式页或GET /v1/games/{game}/format。
null 不是 0
某游戏没有某个概念时字段是 null(例如没有总分概念的游戏 player.rating 为 null)。
请按 null 渲染占位,不要把它当 0 显示。
档案上的只读标记
| 字段 | 说明 |
|---|---|
origin / createdByClientId | 档案由谁创建:user(用户在 RankHub 内建)/ app(第三方建) |
syncCapable | 布尔,只读。不是写权限:false 的档案在 10 个游戏里都能推成绩 |
lastSyncedAt | 时间戳或 null,只读 |
推档案建号
要抄请求体?看写入格式页
身份字段逐游戏不同;player 超集、scores[] 共享字段与可直接抄的 JSON 见
写入格式(建档 / 推成绩),单游戏细节见
各游戏格式页的「写入(第三方)」。
10 个游戏都支持(能力矩阵 pushProfile 全为真)。建档只有一条口径:
调用方自己拿到玩家在该游戏里的身份与资料,直接把字段填进来 —— 服务端只落库,不做别的。
身份字段
player 是超集:字段按游戏生效,不适用字段被忽略;name 恒必填(展示名)。
身份字段逐游戏不同(maimai 的 friendCode / userId、arcaea 的 arcaeaId、
osu 的 osuUserId、chunithm 的街机好友码…)。完整表在
各游戏格式页的「身份字段」,或
GET /v1/games/{game}/format 的 profile.identityFields(含 required 与 example)。
缺失按该游戏既有语义返回 400。
player.rating 只是"总分提示":服务端会用同一请求里的 scores 重算(没有成绩则 0)。
返回的 player.rating 一律以存量为准,不要把你传进去的数字当成已生效。
例子
# maimai:身份是机台好友码;评分由服务端按成绩算
curl -sS -X POST https://api.rankhub.cc/v1/games/maimai/profiles \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"player": { "name": "Player", "friendCode": "1234567890123" },
"scores": [
{ "songId": 1234, "type": "dx", "levelIndex": 3, "achievements": 100.5125 }
],
"idempotencyKey": "b4c0f0d6-8f6f-4d6a-a0d3-3c6f0c4a11"
}'# phigros:身份是 objectId;单曲评分(rks)由你给
curl -sS -X POST https://api.rankhub.cc/v1/games/phigros/profiles \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"player": { "name": "Player", "objectId": "a1b2c3d4-…" },
"scores": [
{ "songId": "…", "difficulty": "IN", "score": 1000000, "accuracy": 100, "rating": 15.2 }
]
}'建档之后的这张档案
- 成功是
201+ 档案对象(data就是上面那张档案); - 建号会写
origin: "app"与createdByClientId;syncCapable为false(只读标记,见上文)。此后这张档案只能靠你推成绩更新; scores可选,对 10 个游戏都生效(与推成绩走同一套"只升不降"合并,见 成绩读取与写入),受单批条数上限约束(超限400 common.invalidArgument);idempotencyKey的作用域是「当前用户 + 当前应用」:重复提交返回同一张档案; 不传就没有幂等保护;- 重复建号返回
409:同一用户在同一游戏已有未解绑档案、或该身份已被占用时, 按该游戏既有唯一索引语义返回409 common.conflict(个别游戏沿用既有400; 文案也沿用既有服务,如… is already linked)。这不是"重试就能过"—— 正确做法是更新已有档案(推成绩),不要反复新建。
修改档案(PATCH)
PATCH /v1/games/{game}/profiles/{profileId}可改展示与可见性字段,至少给一个:
Prop
Type
- 成功后返回更新后的档案对象(与
GET .../profiles/{profileId}同形); - 身份字段不可改:
native里出现身份字段(userId/friendCode/arcaeaId/osuUserId…)且与现有值不同 ⇒400 common.invalidArgument; - 需要
game:write;不是你的档案 / 游戏不存在 ⇒404; - 改
isPublic只影响别人能否读到,不影响你自己读。
删除档案:第三方只能申请
第三方应用不能直接删除档案。删除是"用户对自己数据的处置",必须用户本人在 RankHub 里点头。
直接对档案发 DELETE(不管 /v1 还是 /v0 的档案路径)会得到 403 game.deletionConsentRequired。
提交申请
curl -sS -X POST https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/deletion-requests \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "reason": "用户在我们这里注销了" }'reason 可选(≤ 500 字)。成功是 202 + code: "common.accepted":
{
"code": "common.accepted",
"data": {
"requestId": "pdr_9f2c1e4a77b34d0e",
"status": "pending",
"approveUrl": "rankhub://profiles/deletion?token=pdr_tok_…",
"expiresAt": "2026-10-25T18:40:00Z"
}
}- 平台会给档案 owner 推一条通知,
approveUrl是那条通知里的 deep link(带一次性令牌); - 申请默认 72 小时过期,过期即作废(
status: "expired"),需要重新提交; - 幂等的是"申请",不是"令牌":同一档案 + 同一应用已有
pending申请时,再次 POST 仍返回同一条202,但approveUrl里的令牌会轮换(旧链接立即失效)。 这样重复申请不会堆出第二张待办,同时旧链接也不会长期可用。
查询状态
curl -sS https://api.rankhub.cc/v1/games/maimai/profiles/$PROFILE_ID/deletion-requests/$REQUEST_ID \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"code": "common.ok",
"data": {
"requestId": "pdr_9f2c1e4a77b34d0e",
"status": "pending",
"requestedAt": "2026-10-22T18:40:00Z",
"expiresAt": "2026-10-25T18:40:00Z",
"decidedAt": null
}
}status | 含义 |
|---|---|
pending | 等用户处理(可继续轮询;别把轮询做得比一分钟还密) |
approved | 用户同意,档案已软解绑(不再出现在列表里,历史成绩仍保留) |
denied | 用户拒绝,档案不动 |
expired | 超时未处理,作废;需要就重新提交 |
查询只能看到你自己发起的那一条:不存在的 requestId、别人的申请、以及别的应用对同一档案发的申请
一律 404 common.notFound(不区分,避免被拿来探测)。
没有集合查询(一次列出该档案所有申请):请自己保存 requestId。
用户侧的表态
用户在 RankHub App 里批准/拒绝(.../decision)是第一方专用:第三方调用返回
403 game.firstPartyOnly,不随灰度开关变化。你只需提交申请并轮询状态。
公开档案(社交面)
用户把档案设为公开后,其他登录用户可以在 RankHub 社交面读到同一份数据
(GET /v0/social/users/{uid}/game-profiles/{game} 等)。
社交面本期不开放给第三方
/v0/social/** 只服务第一方(谱面评论是唯一的过渡例外)。第三方令牌打这些路径会拿到
403 game.firstPartyOnly —— 你只能读授权用户本人的档案,读不到其他人的公开档案。
需要"看别人档案"的产品形态,本轮请不要基于社交面设计。
写操作会检查账号状态
新建 / 推成绩 / 申请删除都要求账号没有处于注销流程(accountDeletionRequestedAt
或 accountDeletedAt 非空 ⇒ 403)。这类失败不要重试,也不要循环刷 ——
注销中的账号,用户已经明确表示不再使用。