约定

所有接口共用的规则:引用号、请求体、响应包装、分页、时间、错误码和限流。

最后更新

概述

  • 请求和响应都是 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。

错误码

codeHTTP何时返回
param_missing400缺少必需的参数
param_invalid400查询参数格式错误或越界
pagination_error400分页参数不合法
unauthorized401没有凭据、凭据无效或已撤销,或路径里的团队不是凭据所属的团队
forbidden403已认证,但没有权限
account_read_only403团队的订阅已到期,团队变为只读,写操作被拒绝
not_found404不存在,或已在回收站
conflict409状态冲突,例如删除还有任务的任务状态
content_conflict409正文在你读取之后被改过,见修改正文时带上版本号
participants_conflict409日程的参与者名单已被改过,重新读取后再保存
cursor_regressed409保存的通知游标早于已保存的位置
payload_too_large413请求体超过接口的上限,例如上传的文件超过 100 MB
validation_error422校验失败,看 errors
rank_conflict422排序位置放不下,重新读取后再试
rate_limited429超过限流,按 Retry-After 等待
internal_error500服务器内部错误

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 字符,以字母或数字开头,其余只能是字母、数字、点、下划线、加号、减号和空格;不合法的值会被忽略。它们不影响认证和权限。