约定
所有接口共用的规则:引用号、请求体、响应包装、分页、时间、错误码和限流。
概述
- 请求和响应都是 JSON,字段名用
snake_case。 - 内部 id(
id和各种*_id)是 UUID 字符串。 - 时间是带
Z的 ISO 8601,例如2026-05-28T03:14:22Z;只有日期的字段(如due_date)是YYYY-MM-DD。 - 布尔值是
true/false;空集合是[],不是null。 - 已知不存在的值是
null;不适用的字段直接省略。 - 新字段随时可能出现,客户端要忽略不认识的字段。
引用号
群历里的每一条内容都有一个引用号(public_id):类型前缀加团队内递增的数字,例如 T1042。网页地址里看到的就是它:qunli.co/2025/T1042 里的 T1042。
| 前缀 | 类型 |
|---|---|
T | 任务 |
M | 公告和讨论 |
D | 文档 |
G | 目标 |
S | 日程 |
F | 文件 |
B | 文件夹 |
E | 转发的邮件 |
C | 评论 |
U | 成员(人和机器人) |
- URL 路径里一律用引用号,例如
GET /api/v1/accounts/2025/tasks/T1042、POST /api/v1/accounts/2025/recordings/T1042/comments。 - 引用号不区分大小写,可以带前导
#:T1042、t1042、#T1042是同一个。 - 在一个团队里,一个引用号只指一条内容;前缀不对时返回
404,不会误中别的内容。 - 不知道类型时,用
GET /api/v1/accounts/{account_id}/recordings/{ref}读取任意引用号,返回它的完整内容。 - 内容在项目之间转移后,引用号不变。
- 请求体和响应体里的关联字段(
parent_id、task_status_id、assignee_id……)用 UUID,不用引用号。 - 成员引用号(
U12)可以用在/accounts/{account_id}/users/{id}的id里;同一个人在不同团队里的号码不同。
account_id 是团队的数字 id,例如 2025,和网页地址里的数字相同。GET /api/v1/accounts 返回你能访问的团队,其中的 public_id 就是它。
请求体
创建和修改时,字段包在资源名下面:
{ "project": { "name": "官网改版" } }
任务、文档、讨论、目标、日程、评论这类内容,共用字段放在 recording 下面,各自的字段放在类型名下面:
{
"project_id": "5f0e…",
"recording": { "title": "整理验收清单", "content_markdown": "先列出**必须**通过的项。" },
"task": { "assignee_id": "8a1b…", "due_date": "2026-11-20" }
}
正文用 Markdown
- 正文写在
content_markdown里(GitHub 风格的 Markdown)。服务器根据它生成content_html。content_html和content_json作为输入已弃用。 - 要提及一个成员,在正文里贴他的成员链接,例如
https://qunli.co/2025/U12:对方会收到通知,页面上显示为「@名字」。写成[文字](链接)只是普通链接,不会通知。 - 贴内容的链接(
https://qunli.co/2025/T1042)会显示为那条内容的实时标题。 - 正文最大 128 KB(按 UTF-8 字节算,不是字符数),超过返回
422 validation_error。长内容请作为附件上传,或拆成几篇文档。
修改正文时带上版本号
每条内容都有 content_version,正文每改一次加一。修改正文时带上你读到的版本:
{ "recording": { "content_markdown": "…", "content_version": 7 } }
如果别人在你读取之后改过正文,请求返回 409 content_conflict,什么也不写入,响应里带当前的 content_version。重新读取,把你的修改合并进新正文,再带新版本号重试。不带 content_version 时,后写入的会直接覆盖。只改标题或其他字段不会冲突。
响应
成功的响应一律包在 data 里:
{ "data": { "id": "0d4c…", "public_id": "T1042", "title": "整理验收清单" } }
- 创建成功返回
201,其余成功返回200。 - 没有内容可返回时是
200和{ "data": null },不用204。 - 删除、归档这类改状态的操作,返回改动后的对象。例如删除任务返回状态为
trashed的任务。删除的记录进项目回收站,30 天内可以恢复,之后自动彻底删除;接口见记录、评论与互动里的「回收站」。 - 文件下载、预览和头像这类接口返回
302跳转到文件地址,不套data。
判断成功还是失败看 HTTP 状态码,不要看响应体的形状:status >= 400 时按下面的错误格式解析,否则读 data。
列表的正文
内容列表(任务、讨论、文档、文件、日程、目标……)接受 content 参数:
content=full(目前的默认值):每一行带完整正文。content=summary:不带正文和content_version,只带摘要字段preview_text、excerpt(文档还有preview_html)。需要正文时再读单条。
默认值将来可能改成 summary。需要正文时请明确传 content=full。
分页
分页的列表用 page(从 1 开始)和 limit,响应里带 meta:
{
"data": ["…"],
"meta": { "page": 1, "limit": 30, "total": 142, "total_pages": 5, "has_next": true }
}
limit的默认值和上限因接口而异,见各接口的参数说明。meta.has_next为true时,用page + 1继续读。page不合法时返回400 param_invalid。- 没有
meta的列表不分页,一次返回全部。
通知用游标
GET …/notifications?after=<cursor> 按游标读取,从旧到新返回游标之后的通知,meta 是 { limit, has_more, next_cursor }。下次请求把 next_cursor 作为 after 传入;has_more 为 true 时马上再读一次。游标是不透明的字符串,不要自己拼。after 不能和 page 一起用。
游标也可以存在服务器上:GET 和 PUT …/notifications/cursor。每个成员在每个团队一个游标,只能前进;存一个更早的游标返回 409 cursor_regressed,响应里带已保存的游标。机器人的完整工作循环见通知与搜索和机器人接入指南。
错误
所有 4xx 和 5xx 响应都是 RFC 9457 问题详情,Content-Type: application/problem+json,外面没有包装:
{
"type": "/errors/validation_error",
"title": "Validation failed",
"status": 422,
"detail": "Task title can't be blank",
"instance": "/api/v1/accounts/2025/tasks",
"code": "validation_error",
"trace_id": "01HX…",
"errors": [{ "field": "title", "code": "blank", "message": "can't be blank" }]
}
| 字段 | 说明 |
|---|---|
code | 稳定的错误码,程序按它分支 |
status | 和 HTTP 状态码相同 |
title | 这类错误的通用名称 |
detail | 这一次的具体说明,可能随时改写,不要用来判断 |
instance | 出错的请求路径 |
trace_id | 请求 id,联系我们排查问题时请附上 |
errors | 字段级错误,只在 validation_error 时出现 |
有的错误码带额外字段:content_conflict 带 content_version,cursor_regressed 带 cursor。
错误码
code | HTTP | 何时返回 |
|---|---|---|
param_missing | 400 | 缺少必需的参数 |
param_invalid | 400 | 查询参数格式错误或越界 |
pagination_error | 400 | 分页参数不合法 |
unauthorized | 401 | 没有凭据、凭据无效或已撤销,或路径里的团队不是凭据所属的团队 |
forbidden | 403 | 已认证,但没有权限 |
account_read_only | 403 | 团队的订阅已到期,团队变为只读,写操作被拒绝 |
not_found | 404 | 不存在,或已在回收站 |
conflict | 409 | 状态冲突,例如删除还有任务的任务状态 |
content_conflict | 409 | 正文在你读取之后被改过,见修改正文时带上版本号 |
participants_conflict | 409 | 日程的参与者名单已被改过,重新读取后再保存 |
cursor_regressed | 409 | 保存的通知游标早于已保存的位置 |
payload_too_large | 413 | 请求体超过接口的上限,例如上传的文件超过 100 MB |
validation_error | 422 | 校验失败,看 errors |
rank_conflict | 422 | 排序位置放不下,重新读取后再试 |
rate_limited | 429 | 超过限流,按 Retry-After 等待 |
internal_error | 500 | 服务器内部错误 |
OAuth 端点(/auth_api/*)的错误格式不同,见认证。
限流
| 范围 | 上限 |
|---|---|
通知相关接口(…/notifications、…/notifications/unread_count、…/notifications/cursor) | 每个 API Key 或 OAuth 授权每小时 360 次 |
| OAuth 动态注册 | 每个 IP 每小时 30 次 |
超过时返回 429 rate_limited,Retry-After 响应头给出要等待的秒数。
另外,一个机器人每小时最多通过提及或指派通知其他机器人 30 次;超过的部分不再通知,请求本身照常成功。
客户端标识
可以发送 X-Client-Name 和 X-Client-Version 请求头,帮助我们排查性能问题,例如 X-Client-Name: my-sync、X-Client-Version: 1.2.0。每个值 1~64 个 ASCII 字符,以字母或数字开头,其余只能是字母、数字、点、下划线、加号、减号和空格;不合法的值会被忽略。它们不影响认证和权限。