RankHub
业务 API

游戏档案

在 /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✅✅✅✅friendCode 或 userId;评分由服务端算
arcaea✅✅✅✅arcaeaId;评分由服务端算
phigros✅✅✅✅objectId(+ 可选 shortId)
milthm✅✅✅✅milthmUserId + region
notanote✅✅✅✅notanoteUid
musedash✅✅✅✅musedashUid
rizline✅✅✅✅playerId
osu✅✅✅✅osuUserId
chunithm✅✅✅✅friendCode(街机好友码,不是 LXNS id)
orzmic✅✅✅✅externalId(本地游戏无官方账号体系)

四列对 10 个游戏全部为真:读、建档、推成绩、申请删除都可用。

  • 建档 = 填身份字段:不需要任何会话 / 玩家文件 / 验证码 / 快照,服务端也不会去上游拉取;
  • 推成绩 = 填评分值:单曲评分由你在 scores[].rating 里给;maimai / arcaea 例外, 这两族服务端自己算分。

这张表只是今天的状态,权威是 GET /v1/games 的能力矩阵(见业务 API 总览)。

读取档案

# 能力矩阵:先看这个游戏支不支持写
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.source):

{
  "profileId": "8d1f4a0e-6c2b-4a1e-9d3f-2b7c5e9a0f11",
  "game": "maimai",
  "player": {
    "name": "Player",
    "rating": 15234,
    "source": { "course_rank": 0, "class_rank": 0, "star": 0 }
  },
  "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归一后的显示名与该游戏的"总分"(maimai 的 rating、arcaea 的 potential…);没有总分概念时为 null
player.source游戏原生的玩家对象,未归一,仅供展示/调试;字段随游戏与版本增减
isPublic是否允许出现在公开 / 社交面
originuser = 用户在 RankHub 自己建的;app = 第三方应用建的
createdByClientId建号应用的公开 client id(cli_…);origin: "user" 时为 null
syncCapable该档案当前是否具备同步凭据(详见下节)
lastSyncedAt最近一次从游戏平台同步的时间;从未同步过为 null
createdAt / updatedAt服务端时间(RFC3339)

null 不是 0

某游戏没有某个概念时字段是 null(例如没有总分概念的游戏 player.rating 为 null)。 请按 null 渲染占位,不要把它当 0 显示。

来源标记与同步能力

第三方建的档案会在下一次读取时露出两个字段:

  • origin: "app" + createdByClientId: 告诉用户与你自己"这张档案是谁建的";
  • syncCapable: 该档案当前是否具备同步凭据。

syncCapable 与 origin 无关:它只看"账号级凭据是否在 RankHub 手里"。

  • 建档不会写入任何凭据(你能填的只有身份字段与成绩)⇒ 你建的档案默认 syncCapable: false,此后只能靠你推成绩更新(POST .../{profileId}/scores,10 个游戏都支持);
  • 用户在 RankHub App 里绑定该游戏的账号 / 凭据之后,syncCapable 才变成 true, 之后由 App 侧同步刷新 —— 那部分对你不可见,也不需要你参与;
  • 唯一的例外是"用户此前已经绑过该游戏的账号级凭据":这时你新建的档案可能直接 就是 true(同步能力是账号级的,不是这张档案的)。所以不要假设它一定为 false,一律读字段。

各游戏看的是什么:

游戏syncCapable 的判据
arcaea / orzmic恒为 false(这两个上游没有可长期持有的同步凭据)
phigros / musedash / rizline看档案行上的密文列(用户在 RankHub 绑定后写入)
maimai / chunithm / osu / milthm / notanote看用户级 external account(绑定后写入)

不要用 syncCapable 当写权限

syncCapable 只影响"能不能自动同步",不影响"能不能写":false 的档案在 10 个游戏里 都能推成绩(maimai / arcaea 也一样)。

推档案建号

10 个游戏都支持(能力矩阵 pushProfile 全为真)。建档只有一条口径: 调用方自己拿到玩家在该游戏里的身份与资料,直接把字段填进来 —— 服务端只落库, 不发起任何上游请求、不保存任何凭据。

所以这里没有"材料"这一说:你不需要(也不能)传会话 token、玩家文件、短信验证码或完整快照。 需要登录 / 验证码 / 二维码 / 机台中转的那些流程,属于 RankHub App 里的第一方票据式建档 (POST /v1/games/{game}/profiles/create),对你的应用仍然是 403 game.firstPartyOnly。

身份字段

player 是超集:字段按游戏生效,不适用字段被忽略;name 恒必填(展示名)。 每个游戏必须给出下表中的身份字段,缺失按该游戏既有语义返回 400:

游戏身份字段(必填其一)
maimaifriendCode 或 userId(机台侧 id);可选 courseRank / classRank / star
arcaeaarcaeaId;可选 character / potential / avatar / nameplate
phigrosobjectId(可另给 shortId)
milthmmilthmUserId + region(缺省 cn)
notanotenotanoteUid
musedashmusedashUid
rizlineplayerId
osuosuUserId
chunithmfriendCode(❗ 街机侧身份就是好友码,不是 LXNS 的 id)
orzmicexternalId(本地游戏没有官方账号体系,值由你与用户约定)

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)。这不是"重试就能过"—— 正确做法是更新已有档案(推成绩),不要反复新建。

为什么不再需要「材料」

第三方能拿到的只有"你在自己一侧已经看到的数据"。任何需要替用户登录游戏才能拿到的东西 (会话 token、验证码、二维码、机台中转)都不属于开放平台——那类操作留在 RankHub App, 由用户自己在第一方完成。

删除档案:第三方只能申请

第三方应用不能直接删除档案。删除是"用户对自己数据的处置",必须用户本人在 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 里点通知后,客户端会调 POST /v1/games/{game}/profiles/{profileId}/deletion-requests/{requestId}/decision, body 是 { "decision": "approve" | "deny", "token": "pdr_tok_…" },返回 200 + 该申请的最新状态 (requestId / status / decidedAt 等)。

这条端点不对第三方开放:你的应用拿它去调会得到 403 game.firstPartyOnly, 而且这个限制不受灰度开关影响 —— "删除必须用户同意"是对外承诺,不是灰度里可以临时放宽的行为。 批准后执行的是与用户在 App 里直接删档案完全一致的软解绑。

第一方专用的建档 / 导入 / 同步

下面这些入口都需要用户的游戏平台凭据或设备环境,所以只对 RankHub 自有客户端开放。 第三方令牌调用会得到 403 game.firstPartyOnly(补 scope 也无效)。

⚠️ 注意区分两条看起来相近的路径:POST /v1/games/{game}/profiles(填身份字段,第三方可用) 与 POST /v1/games/{game}/profiles/create(票据式,要替用户登录游戏,第一方专用)。

形状用途
POST /v1/games/{game}/profiles/create票据式建档(多步收集:扫码、验证码、所有权挑战…)
POST /v1/games/{game}/profiles/import从游戏平台导入(异步任务)
POST /v1/games/{game}/profiles/{profileId}/sync同步已有档案(异步任务)
/v1/tasks/**任务查询 / 取消 / 重试
/v1/games/maimai/relay/**maimai 机台中转(App 直连机台)

各游戏的凭据形态(第三方拿不到,但你需要知道为什么拿不到,好在文档里向用户解释):

游戏第一方要怎么拿到数据
maimai机台好友码 / userId,或经 App 的机台中转
arcaea好友码 + 所有权挑战
phigros二维码登录令牌
milthm玩家文件 v2 / 云端导入
musedash导入 + 验证码
notanote云端导入
osu官方 OAuth 导入
rizline手机号 + 验证码
chunithmLXNS 导入
orzmic本地快照导入

给你的用户的正确说法是:"要自动同步,请在 RankHub 里绑定这个游戏", 而不是"让用户把密码/验证码填进我们的表单" —— 后者既不安全,平台也不会接受。

公开档案(社交面)

用户把档案设为公开后,其他登录用户可以在 RankHub 社交面读到同一份数据 (GET /v0/social/users/{uid}/game-profiles/{game} 等)。

社交面本期不开放给第三方

/v0/social/** 只服务第一方(谱面评论是唯一的过渡例外)。第三方令牌打这些路径会拿到 403 game.firstPartyOnly —— 你只能读授权用户本人的档案,读不到其他人的公开档案。 需要"看别人档案"的产品形态,本轮请不要基于社交面设计。

写操作会检查账号状态

新建 / 推成绩 / 申请删除都要求账号没有处于注销流程(accountDeletionRequestedAt 或 accountDeletedAt 非空 ⇒ 403)。这类失败不要重试,也不要循环刷 —— 注销中的账号,用户已经明确表示不再使用。

下一步

本页内容