# 2Libra 开放 API v1

> **API 版本**：v1（`/api/v1`）
> **文档版本**：v1.0 / 2026-11-10
> **基地址**：`https://2libra.com/api`
> **本文件**：文档索引与通用约定。动手前先读完本页；具体端点按下面「怎么读」的指引按需打开分册。

---

## 一、把这段发给你的 AI 助手

下面这段可以直接复制给你的 AI 编码助手（Claude Code / Cursor / ChatGPT / Copilot 等），
它会按本套文档接入 2Libra 的登录与开放 API。**只填尖括号里的内容，不要把 `client_secret` 粘进对话。**

```text
请阅读 2Libra 开放 API 接入文档后再动手：
https://r2.2libra.com/openapi/v1/index.md
（index 会告诉你在需要时去读哪一份分册）

我的应用：
- 形态：<有服务端 / iOS / Android / CLI>   ← 四选一
- client_id：<审核通过后再填>
- redirect_uri：<我在 2Libra 注册的那个回调地址>

硬性要求：
1. 参数名、路径、错误码严格照文档写，不要用通用 OAuth 的常识去推测。
   已知反常识点：所有响应（含 /api/oauth/token）都包在 {c,m,d,t} 信封里；
   换 token 需要 client_secret 与 PKCE code_verifier 同时在场（机密客户端），
   或仅 PKCE code_verifier（公开客户端）。
2. client_secret 只放服务端环境变量，不要写进源码、不要要求我把它粘贴到对话里。
3. 先输出「我需要手动完成的事项清单」（申请应用、注册回调、配置环境变量），确认后再写代码。
4. 最后用 GET /api/v1/me 做一次连通性验证，并说明如何验证 refresh 轮换。

我的具体需求：<在这里写你要做的事，例如「做一个能发帖和看通知的网页」>
```

> **本文档用于「构建第三方应用」。** 若你要在**没有浏览器**的环境里让 agent 运行时调用 API，
> 请让用户在 2Libra 网页完成授权，再由服务端把令牌交给 agent —— 本文档不提供设备码流程。

---

## 二、这套文档怎么读

| 文件 | 内容 | 什么时候读 |
| --- | --- | --- |
| **index.md**（本页） | 通用约定、认证总览、错误码、端点总表 | **必读** |
| [oauth.md](https://r2.2libra.com/openapi/v1/oauth.md) | 授权流程、PKCE、令牌、刷新、撤销、排查 | 要「以用户身份」调用接口时 |
| [api-read.md](https://r2.2libra.com/openapi/v1/api-read.md) | 全部读端点：参数、分页、响应字段、样例 | 要展示内容时 |
| [api-write.md](https://r2.2libra.com/openapi/v1/api-write.md) | 发帖 / 评论 / 互动：逐条请求体字段表 | 要写数据时 |
| [api-economy.md](https://r2.2libra.com/openapi/v1/api-economy.md) | 金币、签到、打赏、盲盒、竞猜、付费墙、上传 | 涉及金币或高级能力时 |
| [terms.md](https://r2.2libra.com/openapi/v1/terms.md) | **第三方应用接入说明**：数据使用边界、内容与版权、金币与免责、违规处置 | **接入前必读** |
| [changelog.md](https://r2.2libra.com/openapi/v1/changelog.md) | 面向第三方的变更记录 | 判断某字段是否将要变化时 |
| [full.md](https://r2.2libra.com/openapi/v1/full.md) | 以上全文拼接版 | 工具支持大上下文、可一次性读入时 |

**给你的 AI 工具的建议**：先读本页；需要具体端点时再打开对应分册。不要一次读 `full.md`
然后凭记忆写代码 —— 里面 80 个端点的字段名很容易混。

阅读顺序建议：**本页 → oauth.md（如果要做登录）→ terms.md（接入前必读）→ 需要的端点分册**。

---

## 三、你能接入的三类形态

| 形态 | 是否需要自己的服务端 | 说明 |
| --- | --- | --- |
| **只读公开内容** | 不需要 | 不带任何令牌直接请求 `/api/v1/*` 的读端点。无需注册应用、无需 OAuth。 |
| **以用户身份读写（有服务端）** | 需要 | 申请时选择「机密客户端」，审核通过后获得 `client_secret`，只放在服务端。 |
| **以用户身份读写（无服务端）** | 不需要 | 原生 App / CLI / 桌面端。申请时选择「公开客户端」，不发 secret，换 Token 靠 PKCE。 |

⚠️ **浏览器 SPA 的限制**：`/api/v1/*` 的跨域策略只放行 `GET` / `HEAD` / `OPTIONS`。
纯浏览器应用可以**读**（含带令牌的读），但**写操作必须经由你自己的服务端转发** ——
写权限包含花金币，把令牌放在 `localStorage` 里经不起一次 XSS。

---

## 四、通用约定

### 4.1 基地址

```
开放 API      https://2libra.com/api/v1
OAuth 端点    https://2libra.com/api/oauth/*
授权页面      https://2libra.com/oauth/authorize     （浏览器跳转，不是 JSON 接口）
媒体资源      https://r2.2libra.com
```

### 4.2 响应信封

**所有接口**（包括 `/api/oauth/*`）的响应都包在同一个信封里：

```json
// 成功
{ "c": 0, "m": "请求成功", "d": { }, "t": 1730000000000 }

// 失败（/api/v1/* 额外带机器可读的 code）
{
  "c": 403,
  "code": "insufficient_scope",
  "m": "缺少权限范围：write:economy",
  "d": null,
  "t": 1730000000000,
  "u": "/api/v1/topics/abc123/reaction"
}
```

出错时还会带上细分上下文（与 `code` 同级，直接在响应根上）：`field`（参数名）、
`required_scope`（缺失的 scope 数组）、`scope` / `limit` / `remaining`（经济额度）、
`dimension` / `retry_after`（限流）。

| 字段 | 含义 |
| --- | --- |
| `c` | `0` 表示成功；否则等于 HTTP 状态码 |
| `m` | 可直接展示给用户的中文提示。**不要用它做逻辑判断** |
| `code` | 机器可读错误码，**只在 `/api/v1/*` 下出现**。逻辑判断只依赖它 |
| `d` | 业务数据。失败时为 `null` |
| `t` | 毫秒时间戳 |
| `u` | 出错时的请求路径。只出现在错误响应里 |

> **最常见的一个坑**：`POST /api/oauth/token` 的令牌**也在 `d` 里面**，不是顶层字段。
> `d.access_token`，而不是 `access_token`。

### 4.3 HTTP 状态码

`/api/v1/*` 返回**真实状态码**（401 就是 401，403 就是 403，429 就是 429）。
重试、退避、刷新令牌的逻辑都应基于状态码 + `code`，不要去猜。

`POST` 创建资源时返回 `201`（发帖、发动态、发评论、上传图片），其余写操作返回 `200`。

### 4.4 错误码表

| `code` | HTTP | 含义 | 客户端该做什么 |
| --- | --- | --- | --- |
| `invalid_request` | 400 | 参数错误，通常带 `field` | 修参数，不要重试 |
| `invalid_grant` | 400 / 401 | 授权码或刷新令牌无效、已用、不匹配 | 重新走授权流程 |
| `unauthorized` | 401 | 令牌无效 / 过期 / 被撤销 / 账户被停用 | 先尝试刷新；刷新失败则重新授权 |
| `insufficient_scope` | 403 | 缺少 scope，带 `required_scope` | 提示用户重新走一次授权流程（用户可能只移除了其中几项） |
| `permission_denied` | 403 | 用户级权限被禁（如被限制发言） | 直接展示 `m`，不要重试 |
| `spend_limit_exceeded` | 403 | 今日金币变动额度用尽，带 `scope` / `limit` / `remaining` | 展示提示，次日再试 |
| `client_suspended` | 403 | 应用被停用 | 联系应用维护者 |
| `not_found` | 404 | 资源不存在或不可见 | 不要重试 |
| `conflict` | 409 | 状态冲突（重复签到、已收藏等） | 按业务语义处理 |
| `idempotency_in_progress` | 409 | 同一 `Idempotency-Key` 正在处理 | 1 秒后重试同一请求 |
| `idempotency_key_reuse` | 409 | 同一 `Idempotency-Key` 配了不同请求体 | 换一个 Key |
| `rate_limited` | 429 | 触发配额，带 `dimension` / `limit` / `retry_after` | 按 `retry_after` 秒退避 |
| `internal_error` | 500 | 服务端错误 | 指数退避重试 |

`insufficient_scope`（去申请权限）与 `permission_denied`（用户被禁言）**都是 403**，
只能靠 `code` 区分 —— 这是必须依赖 `code` 而不能只看状态码的原因。

### 4.5 认证

```
Authorization: Bearer <access_token>
```

- 读端点**允许匿名**：完全不带 `Authorization` 头即可。
- 匿名 vs 登录的差异：带令牌时会多返回个性化字段（`is_following`、`voted`、`favorited` 等）。
- **带了令牌但不合法 → 401**，绝不静默降级为匿名。收到 401 应该去刷新或重新授权。
- **不要发送空的 `Authorization` 头**（`Authorization: Bearer ` 也会被判定为 401）。

令牌怎么拿、怎么刷新、怎么撤销，见 [oauth.md](https://r2.2libra.com/openapi/v1/oauth.md)。

### 4.6 配额与限流

| 场景 | 额度 |
| --- | --- |
| 匿名读 | 每 IP、每分钟、**每路由** 60 次 |
| 登录读/写 | 每 (应用, 用户) 每小时 600 次 |
| 单应用总量 | 每小时 20000 次 |
| 搜索 | 匿名 5 次/分；登录 20 次/分 |
| 上传图片 | 每人每天 20 张 |
| 换头像 | 每人每天 2 次 |
| 金币变动 | 每人每天 3000；每应用每天 20000 |

- 超限返回 `429` + `code: "rate_limited"`，错误体里有 `retry_after`（秒）。
- 配额按「应用 × 用户」拆分而不是按 IP 聚合 —— 大量用户共用同一个出口 IP 不会互相挤占。
- 请自行做**请求去重与缓存**：同一份列表不必每次刷新都重新拉。

### 4.7 分页

页码分页：`?page=1&limit=20`

- `page` 从 1 开始
- `limit` 上限因端点而异：多数是 50，`/v1/topics` 与评论列表最多 100，`/v1/scores` 最多 30
- ⚠️ **列表字段名不统一**：帖子列表是 `posts`，通知列表是 `list`，评论 / 书签 / 关注流是 `items`，
  节点 / 标签 / 表情 / 徽章直接是数组。具体以各分册的示例为准，
  **不要写一个通用的「取 items」函数**。
- ⚠️ **帖子流在持续插入数据时，翻页可能重复或漏掉条目**。这是页码分页的固有代价，
  我们明确不承诺「不重不漏」。需要去重时请按资源 ID 自行处理。

### 4.8 幂等

标注 🔑 的写操作**必须**带 `Idempotency-Key` 请求头，否则返回 `400 invalid_request`。

```
Idempotency-Key: 5f2c1a9b0e7d4c3a
```

- 格式：≤255 字符，只允许 `[A-Za-z0-9_-]`
- 同一 Key + 同一请求体 → **只执行一次**，重复请求原样回放第一次的结果
- 同一 Key + 不同请求体 → `409 idempotency_key_reuse`
- 上一次请求还在处理中 → `409 idempotency_in_progress`，1 秒后重试即可
- Key 的作用域是「应用 × 用户 × 路由」，不同的用户/端点之间不会撞

**什么时候必须换新 Key**：用户点了「发送」并成功之后，下一次新的发帖要用新 Key。
网络超时重试则必须用**同一个** Key。

### 4.9 ID 与字段口径

| 资源 | 对外标识 |
| --- | --- |
| 帖子 | `short_id`（如 `a1b2c3d4`） |
| 用户 | `username`（徽章相关端点也接受用户 uuid） |
| 节点 | `slug` |
| 评论 | uuid |
| 徽章 | uuid |
| 投票 / 投票选项 / 竞猜选项 | uuid |

用户对象里还带着内部 uuid `id`。它**不用于拼链接或做跨端标识** —— 用户唯一公开标识是 `username`。
但发表情表态、关注等端点需要对方的 uuid，这时从响应的 `id` 字段取。

字段命名不统一是历史事实，**必须照抄**，不要自行改写：

- 绝大多数请求体用 `snake_case`（`short_id`、`node_id`、`redirect_uri`）
- 三个 DTO 用 `camelCase`：`POST /v1/topics/:short_id/vote` 的 `voteId` / `optionId`，
  `POST /v1/topics/:short_id/vote/multi` 的 `voteId` / `optionIds`，
  表态端点的 `recUserId` / `nodeId` / `postTitle` / `locatedFloor`

> **硬规则：文档中出现的接口路径、参数名、字段名、错误码必须原样使用，
> 不要翻译成中文、不要改写大小写、不要「顺手统一」成 snake_case。**

### 4.10 媒体地址已经是绝对地址

服务端已经拼好，**拿到直接用，不要自己再拼一遍**：

- `avatar_url` → `https://r2.2libra.com/avatars/...`
- 表情 `img_url` → `https://r2.2libra.com/emojis/sigh.png`
- 徽章 `icon_url` → 小图（固有尺寸 22×15），用于用户名旁的 ≤32px 槽位
- 徽章 `icon_url_large` → 大图（256×255），用于弹窗 80px / 卡片 64px。
  **放大显示必须用大图**，否则 WebKit 会按 22px 栅格化，放大后发糊。

正文里的表情是 `:collector:` 这类短码，必须用 `GET /v1/emojis` 的映射表渲染成图片。

### 4.11 未知字段必须忽略

我们承诺**随时可以新增**字段、端点、scope。客户端解析响应时必须忽略不认识的字段，
并按「字段可能新增、不会被删除」来设计。这一条是 v1 兼容承诺的一部分。

### 4.12 不会被下发的字段

响应在服务端做过深度裁剪，以下字段**在任何嵌套层级都不会出现**：
`email`、`password`、二步验证相关、`is_banned`、风控权重、IP、登录与设备信息、
内部备注、`refresh_token`、`client_secret`。

需要用户邮箱的客户端请改用其它方式确认身份（v1 的 `email` scope 未开放）。

---

## 五、端点总表（80 条）

`scope` 列表示该端点要求的权限范围。标「—」的可匿名调用（带令牌则返回个性化字段）。
标「需开通」的端点属于高级能力，需要在申请时说明用途并由人工审核开通。

| # | 方法 | 路径 | scope | 说明 | 详见 |
| --- | --- | --- | --- | --- | --- |
| 1 | GET | `/v1/nodes` | — | 节点（板块）列表，`?parent=<slug>` 取子节点 | [read](https://r2.2libra.com/openapi/v1/api-read.md#节点) |
| 2 | GET | `/v1/nodes/:slug` | — | 单个节点详情 | [read](https://r2.2libra.com/openapi/v1/api-read.md#节点) |
| 3 | GET | `/v1/topics` | — | 帖子列表（节点 / 标签 / 维度 / 排序） | [read](https://r2.2libra.com/openapi/v1/api-read.md#帖子) |
| 4 | GET | `/v1/topics/latest` | — | 最新帖子流 | [read](https://r2.2libra.com/openapi/v1/api-read.md#帖子) |
| 5 | GET | `/v1/topics/hot` | — | 热门帖，`?period=24h\|7d` | [read](https://r2.2libra.com/openapi/v1/api-read.md#帖子) |
| 6 | GET | `/v1/topics/:short_id` | — | 帖子详情（正文、投票、竞猜、附言、反应） | [read](https://r2.2libra.com/openapi/v1/api-read.md#帖子) |
| 7 | GET | `/v1/topics/:short_id/replies` | — | 楼层评论，`?flat=1` 返回扁平时间序 | [read](https://r2.2libra.com/openapi/v1/api-read.md#评论) |
| 8 | GET | `/v1/comments/latest` | — | 全站最新评论时间线 | [read](https://r2.2libra.com/openapi/v1/api-read.md#评论) |
| 9 | GET | `/v1/users/:userName` | — | 用户主页资料 | [read](https://r2.2libra.com/openapi/v1/api-read.md#用户) |
| 10 | GET | `/v1/users/:userName/topics` | — | 该用户的帖子 | [read](https://r2.2libra.com/openapi/v1/api-read.md#用户) |
| 11 | GET | `/v1/users/:userName/replies` | — | 该用户的评论 | [read](https://r2.2libra.com/openapi/v1/api-read.md#用户) |
| 12 | GET | `/v1/users/:userNameOrId/badges` | — | 用户徽章墙 | [read](https://r2.2libra.com/openapi/v1/api-read.md#徽章) |
| 13 | GET | `/v1/users/:userNameOrId/badges/equipped` | — | 用户当前佩戴的徽章 | [read](https://r2.2libra.com/openapi/v1/api-read.md#徽章) |
| 14 | GET | `/v1/emojis` | — | 表情短码 → 图片地址（渲染 `:code:` 必需） | [read](https://r2.2libra.com/openapi/v1/api-read.md#表情) |
| 15 | GET | `/v1/users/:userName/collections` | — | 用户公开合集列表 | [read](https://r2.2libra.com/openapi/v1/api-read.md#合集) |
| 16 | GET | `/v1/collections/:collectionId/topics` | — | 合集内帖子 | [read](https://r2.2libra.com/openapi/v1/api-read.md#合集) |
| 17 | GET | `/v1/tags/hot` | — | 今日热门标签 | [read](https://r2.2libra.com/openapi/v1/api-read.md#标签) |
| 18 | GET | `/v1/search` | — | 全站搜索（单独限流） | [read](https://r2.2libra.com/openapi/v1/api-read.md#搜索) |
| 19 | GET | `/v1/coins/ranking` | — | 金币排行榜，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#榜单) |
| 20 | GET | `/v1/scores` | — | 帖子评分榜，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#榜单) |
| 21 | GET | `/v1/scores/categories` | — | 评分分类字典，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#榜单) |
| 22 | GET | `/v1/topics/:short_id/restricted` | `read` | 付费 / 门槛内容，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#付费墙) |
| 23 | GET | `/v1/topics/:short_id/restricted/auction` | `read` | 竞拍式受限内容状态，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#付费墙) |
| 24 | GET | `/v1/topics/:short_id/restricted/auction/bids` | `read` | 竞拍出价记录，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#付费墙) |
| 25 | GET | `/v1/topics/:short_id/bets` | `read:economy` | 竞猜信息，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#竞猜) |
| 26 | GET | `/v1/topics/:short_id/vote/result` | `read` | 投票结果，`?vote_id=` | [write](https://r2.2libra.com/openapi/v1/api-write.md#投票) |
| 27 | GET | `/v1/me` | `profile` | 当前用户完整资料 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 28 | GET | `/v1/me/summary` | `profile` | 精简身份快照（比 `/me` 便宜） | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 29 | GET | `/v1/me/badges` | `profile` | 我拥有的徽章 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 30 | GET | `/v1/me/badges/equipped` | `profile` | 我当前佩戴的徽章 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 31 | GET | `/v1/me/topics` | `profile` | 我的帖子（选择器用，`?q=` 过滤） | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 32 | GET | `/v1/me/notifications` | `read:me` | 通知列表 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 33 | GET | `/v1/me/notifications/unread-count` | `read:me` | 通知未读数 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 34 | POST | `/v1/me/notifications/read` | `read:me` | 全部标记已读 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 35 | GET | `/v1/me/collections` | `read:me` | 我的合集 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 36 | POST | `/v1/me/collections` | `read:me` | 新建合集 | [write](https://r2.2libra.com/openapi/v1/api-write.md#合集) |
| 37 | POST | `/v1/me/collections/assign` | `read:me` | 帖子加入 / 移出合集 | [write](https://r2.2libra.com/openapi/v1/api-write.md#合集) |
| 38 | GET | `/v1/me/follows/timeline` | `read:me` | 关注流 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 39 | GET | `/v1/me/follows/unread-count` | `read:me` | 关注流未读数，`?last_read_at=` | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 40 | GET | `/v1/me/favorites` | `read:me` | 我收藏的帖子 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 41 | GET | `/v1/me/bookmarks` | `read:me` | 我的评论书签 | [read](https://r2.2libra.com/openapi/v1/api-read.md#我的) |
| 42 | GET | `/v1/me/coins/balance` | `read:economy` | 金币余额 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币查询) |
| 43 | GET | `/v1/me/coins/transactions` | `read:economy` | 金币流水 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币查询) |
| 44 | GET | `/v1/me/coins/balance-with-ranking` | `read:economy` | 余额 + 我的排名，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币查询) |
| 45 | GET | `/v1/me/coins/today-transaction` | `read:economy` | 今日收支明细，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币查询) |
| 46 | GET | `/v1/me/coins/daily-flow` | `read:economy` | 收支按日聚合（曲线），需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币查询) |
| 47 | GET | `/v1/me/coins/expense-breakdown` | `read:economy` | 支出分类（饼图），需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币查询) |
| 48 | GET | `/v1/me/sign` | `read:economy` | 今日签到状态 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#签到) |
| 49 | GET | `/v1/pandora/today` | `read:economy` | 今日盲盒状态，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#盲盒) |
| 50 | POST | `/v1/topics` 🔑 | `write:topic` | 发帖（可带投票 / 竞猜 / 兑换码 / 金币池） | [write](https://r2.2libra.com/openapi/v1/api-write.md#发帖) |
| 51 | POST | `/v1/topics/moments` 🔑 | `write:topic` | 发动态（短内容），需开通 | [write](https://r2.2libra.com/openapi/v1/api-write.md#发帖) |
| 52 | POST | `/v1/topics/:short_id/edit` | `write:topic` | 编辑帖子（last-write-wins） | [write](https://r2.2libra.com/openapi/v1/api-write.md#编辑帖子) |
| 53 | POST | `/v1/topics/:short_id/postscript` 🔑 | `write:topic` | 追加附言 | [write](https://r2.2libra.com/openapi/v1/api-write.md#附言) |
| 54 | POST | `/v1/topics/:short_id/comments-status` | `write:topic` | 关闭 / 开启评论（仅作者），需开通 | [write](https://r2.2libra.com/openapi/v1/api-write.md#评论开关) |
| 55 | POST | `/v1/topics/:short_id/vote` | `write:topic` | 单选投票 | [write](https://r2.2libra.com/openapi/v1/api-write.md#投票) |
| 56 | POST | `/v1/topics/:short_id/vote/multi` | `write:topic` | 多选投票 | [write](https://r2.2libra.com/openapi/v1/api-write.md#投票) |
| 57 | POST | `/v1/topics/:short_id/replies` 🔑 | `write:comment` | 发评论 | [write](https://r2.2libra.com/openapi/v1/api-write.md#评论) |
| 58 | POST | `/v1/replies/:id/edit` | `write:comment` | 编辑评论 | [write](https://r2.2libra.com/openapi/v1/api-write.md#评论) |
| 59 | POST | `/v1/topics/:short_id/reaction` | `interact` | 帖子表态（toggle） | [write](https://r2.2libra.com/openapi/v1/api-write.md#表态) |
| 60 | POST | `/v1/replies/:id/reaction` | `interact` | 评论表态（toggle） | [write](https://r2.2libra.com/openapi/v1/api-write.md#表态) |
| 61 | POST | `/v1/topics/:short_id/favorite` | `interact` | 收藏帖子 | [write](https://r2.2libra.com/openapi/v1/api-write.md#收藏) |
| 62 | DELETE | `/v1/topics/:short_id/favorite` | `interact` | 取消收藏 | [write](https://r2.2libra.com/openapi/v1/api-write.md#收藏) |
| 63 | POST | `/v1/replies/:id/bookmark` | `interact` | 书签评论（可带备注） | [write](https://r2.2libra.com/openapi/v1/api-write.md#书签) |
| 64 | DELETE | `/v1/replies/:id/bookmark` | `interact` | 移除书签 | [write](https://r2.2libra.com/openapi/v1/api-write.md#书签) |
| 65 | POST | `/v1/users/:userId/follow` | `interact` | 关注 | [write](https://r2.2libra.com/openapi/v1/api-write.md#关注) |
| 66 | DELETE | `/v1/users/:userId/follow` | `interact` | 取关 | [write](https://r2.2libra.com/openapi/v1/api-write.md#关注) |
| 67 | POST | `/v1/rewards` 🔑 | `write:economy` | 打赏 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#打赏) |
| 68 | POST | `/v1/me/sign` | `write:economy` | 签到 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#签到) |
| 69 | POST | `/v1/me/badges/:badgeId/equip` 🔑 | `write:economy` | 佩戴徽章（可能扣金币），需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#徽章佩戴) |
| 70 | DELETE | `/v1/me/badges/:badgeId/equip` 🔑 | `write:economy` | 取消佩戴，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#徽章佩戴) |
| 71 | POST | `/v1/topics/:short_id/restricted/unlock` 🔑 | `write:economy` | 花金币解锁受限内容，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#付费墙) |
| 72 | POST | `/v1/topics/:short_id/reward-pool` 🔑 | `write:economy` | 给帖子金币池注资（200～1000），需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#金币池) |
| 73 | POST | `/v1/topics/:short_id/bets/:option_id/participate` 🔑 | `write:economy` | 参与竞猜（扣币），需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#竞猜) |
| 74 | POST | `/v1/redeem-codes/:code/redeem` 🔑 | `write:economy` | 领取兑换码，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#兑换码) |
| 75 | POST | `/v1/pandora/open` 🔑 | `write:economy` | 开盲盒，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#盲盒) |
| 76 | POST | `/v1/pandora/reroll` 🔑 | `write:economy` | 盲盒重掷，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#盲盒) |
| 77 | POST | `/v1/pandora/extend` 🔑 | `write:economy` | 盲盒延期，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#盲盒) |
| 78 | POST | `/v1/upload/image` | `upload` | 上传图片 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#上传) |
| 79 | POST | `/v1/me/avatar` | `write:economy` | 换头像（multipart，一步到位），需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#头像) |
| 80 | DELETE | `/v1/me/avatar` | `write:economy` | 重置头像，需开通 | [economy](https://r2.2libra.com/openapi/v1/api-economy.md#头像) |

> 🔑 = 必须带 `Idempotency-Key` 请求头。
> 读端点即使标了 scope，**不带令牌时仍可匿名调用**；只在带了令牌时才要求对应 scope。
> “需开通”不是「不存在」，而是「默认不授予，需要在申请时说明用途」。未获授权时调用会得到
> `403 insufficient_scope`，与端点不存在在安全上等价。

> ⚠️ **接入前请先读 [terms.md](https://r2.2libra.com/openapi/v1/terms.md)**：
> 它写清了数据能怎么用、内容版权归谁、金币损失谁承担，以及什么行为会导致应用被封。
> 提交接入申请即视为接受其中的条款。

---

## 六、兼容性承诺

1. **随时新增**：字段、端点、scope 都可能新增，客户端必须忽略未知字段。
2. **删除或改语义**：先在 changelog 标注 `deprecated`，**至少 90 天后**才移除；期间端点照常工作。
3. **scope 名字永不修改**：`profile` / `read` / `read:me` / `read:economy` / `write:topic` /
   `write:comment` / `interact` / `write:economy` / `upload` 一旦发布就是契约。
4. **信封 `{c,m,d,t}` 不变**；`code` 只增不减。
5. **鉴权失败的三种语义不可互换**：`401 unauthorized`（令牌无效）、
   `403 insufficient_scope`（缺 scope）、`403 permission_denied`（用户级被禁）。
