社区开放 API
这是一套社区级 API:一把密钥绑定一个社区,并以你显式指定的成员身份执行操作。它不是创作者后台 API,也不是成员端 API——这里没有登录态。
开始之前
有两件事必须先成立,任何密钥才会生效,而它们都不是你能在自己代码里解决的。
1. 社区的订阅方案必须包含 API 权限
这一步由社区所有者控制,不是集成方能自助完成的。方案需要同时满足:包含 API 访问能力,否则每次调用返回 22203;以及每日额度不为 0,否则每次调用返回 22204。
较早创建的社区,API 权限默认是关闭的,即使方案看起来已经包含了其他高级功能。如果一把刚签发的密钥每次调用都返回 22203,请让社区所有者去后台确认,而不是反复检查你的代码。
你不必靠撞错误来发现这一点:/capabilities/get 会直接告诉你,而且在上述两种情况下它仍然调得通。
2. 在社区后台创建密钥
社区后台 → 集成 → API Keys → 创建。密钥形如 mfk_ 加 64 位十六进制,完整值只在创建时展示一次,之后只能看到前缀。
创建时勾选的 scope 决定这把密钥能调哪些端点,之后无法修改;要换 scope 只能新建一把密钥并吊销旧的。
可选:给密钥配置 IP 白名单(单 IP 或 CIDR 段)。来源 IP 不在其中的请求会返回 10007——和密钥无效是同一个错误码,当一把昨天还能用的密钥换台机器就不行了的时候,这一点值得记住。
Scope 一览
| Scope | 覆盖范围 |
|---|---|
| site.read | 社区资料与设置 |
| content.read | 帖子读取、信息流、搜索与回复树 |
| content.write | 发帖、改帖、删帖 |
| media.read | 已存储媒体的签名 URL |
| media.write | 媒体上传令牌——出于向后兼容,也可读取签名 URL |
| spaces.read | 空间目录与详情 |
| members.read | 成员读取与邮箱查找 |
* 的语义在 2026 年 9 月改了
它过去表示「全部权限,包括以后新增的」——这意味着平台每上线一项新能力,历史上签发过的每一把密钥都会被悄悄放宽。现在:新建密钥会把创建当时的具体清单落库;而库里残留的 * 只代表它被签发时存在的那四个 scope:site.read、content.read、content.write 与 media.write。
所以老密钥调不了后来新增的端点——空间、成员以及签名 URL 读取。请重新签发一把并勾上这些 scope。这是有意的:没有人应该因为平台上线了一项新功能,就在不知情的情况下把成员目录交出去。
认证
两种请求头任选其一,效果等价;同时传时 X-API-Key 优先。社区由密钥反查得出,所以你永远不需要传 site id。
X-API-Key: mfk_xxx
Authorization: Bearer mfk_xxx
密钥本身即凭据——请只在服务端保存和使用。它既不该出现在浏览器代码里,也不该打进移动端包体:任何能读出它的人都可以以你的成员身份发帖。
通用约定
以下对下面每一个端点都成立。
- 所有端点一律 POST。没有 GET / PUT / DELETE——读取也是 POST。
- 请传 Content-Type: application/json。即使某个端点不需要任何参数,也要传一个空 JSON 对象。
- 所有 id 都是 hashid 字符串,例如 "kZ3mQ9x"——社区、用户、帖子、空间、媒体一律如此。数字 ID 会被拒绝。
- 出参时间为 RFC3339,例如 2026-08-18T10:00:00Z。
- 金额一律是以最小货币单位(分)计的整数。
- 可选的 Accept-Language 请求头(zh、en、ja、ko、es、fr、de、pt)会影响错误文案的语言。
- 社区由你的 API Key 反查得出。不要在路径或请求体里传 site id。
有一个入参不遵守这条规则:/post/create 与 /post/update 的 scheduled_at 是 i64 Unix 秒,不是字符串。见下方「定时发布」。
成功与失败共用同一层信封
{
"code": 0,
"msg": "success",
"data": { }
}端点清单
所有端点都在 /site_open_api/v1 之下,一律 POST + JSON 请求体,并要求密钥带有所列的 scope。
返回这把密钥的 scope、API 权限是否开启,以及额度的已用量、上限与重置时间。这是唯一一个在套餐门禁或当日额度把其他调用全部挡住时仍然调得通的端点,而且它本身不消耗额度。quota_limit 为 -1 表示无限;三个额度字段同进同退,所以在做减法之前先确认字段存在。
社区资料、设置与元信息
按 id 取单条帖子
必填: id
一次最多 50 条;查不到的 id 落在 missing_ids 里,不会让整单失败
必填: ids
搜索帖子——字段名是 query,不是 keyword
必填: query
精选帖子,可选按空间限定
某条帖子下的全部回复
必填: post_id
仅一级回复
必填: post_id
某条回复之下的子树
必填: post_id
某条回复的祖先链
必填: post_id
社区信息流,按时间倒序(字符串游标)。since_id 只拉比它更新的帖子——它看不到修改和删除,所以只能用来给时间线补新内容,不能当增量同步
社区信息流,按热度分排序(浮点数游标)
精选信息流(字符串游标)
单个空间的信息流,支持可选的排序、搜索与问答筛选(字符串游标)
必填: space_id
单个空间的信息流,按热度分排序(浮点数游标)——旧入口,仅为兼容保留
必填: space_id
以成员身份发帖。正文、空间、标题、回复与引用目标、媒体、投票、音频与定时发布均为可选
必填: author_user_id, idempotency_key
编辑帖子;传 version 可做乐观锁
必填: actor_user_id, idempotency_key, post_id
删除帖子
必填: actor_user_id, idempotency_key, post_id
该视角可见的空间——不传 viewer_user_id 即匿名视角,只返回公开可读的那些
按 id 取单个空间
必填: id
按 id 取单个成员
必填: id
用精确的邮箱地址解析出成员
必填: email
媒体上传服务用的短期令牌
必填: author_user_id
私有媒体的签名 URL(会过期——按需重新签发,不要落库保存)
必填: author_user_id, items, access_level
curl -X POST https://api.mateflow.com/site_open_api/v1/capabilities/get \
-H "X-API-Key: mfk_xxx" -H "Content-Type: application/json" -d '{}'
# → data: {
# "api_version": "v1",
# "site_id": "kZ3mQ9x",
# "scopes": ["site.read", "content.read"],
# "api_access": true,
# "quota_limit": 5000, // -1 means unlimited
# "quota_used": 128,
# "quota_reset_at": "2026-09-15T00:00:00Z"
# }- 接受 id 列表的读接口上限为 50 条,所有分页端点的 limit 上限同样是 50。
- /space/feed/list 的游标编码跟随 sort,所以换 sort 就必须重置分页。问答空间会忽略 sort,改按 qa_sort 排序。新接入建议用 /space/feed/list 加 sort=top,而不是 /space/feed/top。
- 成员响应是一份收窄过的投影:id、username、display_name、avatar_url、status、role 与 created_at。邮箱永远不在返回字段里,即使你就是用邮箱查的——那个地址你本来就持有,而把每个成员的地址回显出去会让 /member/get 变成一次通讯录导出。lookup 只做精确匹配,未命中是正常成功而不是错误,所以命中与未命中在状态码和耗时上都无法区分。它也是全 API 里限流最严的一条。
以成员身份操作
这套 API 没有登录态,所以「谁在操作」和「以谁的视角看」永远由请求体显式指定。三者都填成员 hashid。
author_user_id新帖子以哪位成员的身份发布。该成员必须已属于本社区,否则返回 20303。
actor_user_id执行编辑或删除的人。权限按这个人来判定——既不是作者、也不是管理员就会返回 10004。
viewer_user_id读取时可选。以该成员的视角返回结果:私密空间可见性、点赞与收藏状态等。不传即匿名视角,只能看到公开内容。
分页
- 信息流和列表接口会连同 next_cursor 一起返回结果。把 next_cursor 原样回传即可翻到下一页;next_cursor 为空表示已经到底。
- 所有接口的 limit 上限都是 50。
- /space/feed/list 的游标里编码了当前的 sort。翻页途中改 sort 会让游标失效——请退回第一页重新开始。
注意:/feed/top 与 /space/feed/top 的游标是浮点数热度分,不是字符串。用同一个字符串变量去接两种游标,翻页会静默失效。
幂等
所有写接口都要在请求体里传 idempotency_key。这里只有这一层幂等——如果你已经了解平台那套基于请求头的机制,请看下面的提示。
- idempotency_key 在所有写接口都必填,长度不超过 190 字符。同一个 key 重复调用不会产生第二条帖子。
- 它的作用域是「社区 + 请求类型 + 幂等键」,与你用的是哪把 API Key 无关:同一社区的两把不同密钥,用同一个 idempotency_key 调同一个端点,第二次会命中第一次的结果。
- 同一个 key 配不同的请求体会返回一个稳定的冲突错误,而不会被静默当成一次成功的重试。
- 请求超时后重试,请复用同一个 key,绝不要换新的。
平台通用的 Idempotency-Key 请求头——就是带 24 小时响应缓存的那一层——对 /site_open_api/** 不生效。那一层挂在 API Key 鉴权之前,缓存命中会完全跳过密钥校验。不要指望在这里传这个请求头会起作用。
定时发布
定时发布是唯一一处入参格式和行为都与别处不同的地方。
- scheduled_at 是 i64 Unix 秒时间戳——不是其他所有时间字段用的 RFC3339 字符串。
- 在 /post/update 里省略 scheduled_at 表示保持既有排期不变,它不是「清空」。
传 0 或更小的值表示取消排期、把帖子退回草稿——但只有 blog 帖有草稿状态。对普通帖执行同样的调用会返回 10005 与 schedule_cancel_unsupported,因为底层那条路径实际是「立即发布」:接受它等于把内容提前公开、触发信息流与通知,而且事后无法撤销。
上传媒体
文件走独立的媒体服务,不走 API 主机。分三步:
- 通过 /media/get_token 用 API Key 换一个短期上传令牌。
- 以 multipart 形式把文件 POST 给媒体服务,令牌放在字面名为 "token" 的请求头里,file_cate 取 Media、Avatar、Header、Audio 或 File 之一。
- 发帖时把返回的媒体 id 传进 media_ids。
破坏性变更:2026 年 9 月
不显式传 scope 时签发的令牌现在只带 upload 权限;此前是 upload、read、delete 三合一。如果你曾用这个令牌做读取或删除,请显式传 read 或 delete 的 scope。另外,处于宽限期的社区——试用到期且未绑卡——申请 upload 令牌会被套餐门禁拒绝,read 与 delete 不受影响。
# 1. Exchange the API key for a short-lived upload token.
# Since 2026-09 a token with no explicit scope is upload-only.
curl -X POST https://api.mateflow.com/site_open_api/v1/media/get_token \
-H "X-API-Key: mfk_xxx" -H "Content-Type: application/json" \
-d '{"author_user_id": "kZ3mQ9x", "scope": "upload"}'
# 2. Upload the file to the media service (token goes in a "token" header)
curl -X POST https://media.mateflow.com/api/v1/media/upload \
-H "token: <token from step 1>" \
-F "file_cate=Media" -F "file=@photo.jpg"
# 3. Attach the media id when creating the post
curl -X POST https://api.mateflow.com/site_open_api/v1/post/create \
-H "X-API-Key: mfk_xxx" -H "Content-Type: application/json" \
-d '{"author_user_id":"kZ3mQ9x","idempotency_key":"6f1c...","body":"Hi","media_ids":["m8Yq2Lp"]}'私有媒体通过 /media/get_signed_urls 读取,它接受 items 与 access_level(1 公开 / 2 半私有 / 3 私有),返回带过期时间的 URL。请按需签发而不要落库保存,否则它们会开始返回 403。
给帖子附加媒体
- 要么传 medias——一组 media_id 与 alt 的组合——要么传 media_ids,即一个纯 id 列表。两个同传会返回 10005。
- alt 可选。传空串表示清空已有的替代文本;不传该字段表示保持原样。
错误码
成功与失败共用同一层信封,HTTP 状态码与业务 code 联动。请按 code 分支,而不是按状态码。
| Code | HTTP | 含义 |
|---|---|---|
| 0 | 200 | 成功 |
| 10005 | 400 | 参数非法——msg 里会指出出问题的字段名 |
| 10007 | 401 | 密钥无效、已吊销或已过期,或来源 IP 不在白名单内 |
| 21304 | 403 | 密钥缺少该端点所需的 scope;scope 在创建时即固定 |
| 22203 | 403 | 套餐不含 API 权限——见 data.required_plan |
| 22204 | 403 | 当日额度已耗尽——见 data.current 与 data.limit |
| 20303 | 404 | author_user_id / viewer_user_id 不是本社区的成员 |
| 10003 | 404 | 目标记录不存在 |
| 10004 | 403 | 操作者对该资源无权限 |
| 10202 | 429 | 被限流——请退避并遵守 Retry-After 响应头 |
| 10001 | 500 | 服务端错误——可以安全重试 |
注意这个陷阱:22203 与 22204 是 403,不是 429;只有 10202 才是限流。请按 code 判断,绝不要按 HTTP 状态码——这是这套 API 最常见的一处误读。
错误响应会带上 request_id。报告问题时请附上它——我们靠它在日志里定位你那一次具体调用。
套餐类错误带有结构化元数据
两个套餐类错误都会带上足以告诉社区所有者该怎么做的信息,所以你可以给出一条真正有用的提示,而不是「出错了」。
// The plan does not include API access
{
"code": 22203,
"msg": "...",
"request_id": "00de5640-8ae5-4a71-aef8-2f2cab0ce8a7",
"data": {
"error_code": "feature_not_available",
"feature": "api_access",
"required_plan": "Growth"
}
}
// The community is out of daily quota
{
"code": 22204,
"data": {
"error_code": "quota_exceeded",
"resource": "api_requests_per_day",
"current": "5001",
"limit": "5000",
"required_plan": "Business"
}
}配额与速率限制
三套互相独立的机制。把它们混为一谈,正是「某个集成被限流了」其实只是套餐额度用完了的原因。
| 层级 | 统计口径 | 超限时 |
|---|---|---|
| 套餐日配额 | 一整天、全部端点、按社区 | 22204 / HTTP 403 |
| 按密钥的突发预算 | 10 秒与 60 秒窗口,按密钥,并按端点分级 | 10202 / HTTP 429 |
| 通用 IP 限流 | 按来源 IP,10 秒 60 次、每分钟 300 次 | 10202 / HTTP 429 |
日配额统计的是通过鉴权的请求,UTC 零点重置,并在 data.limit 里给出实际生效的上限——包括平台为该社区单独放宽的额度。计数器后端异常时会放行而不是拒绝,所以偶发的少计不代表配额失效了。
按密钥的突发预算(2026 年 9 月新增)
此前唯一的桶是按 IP 的:同一出口地址背后的多个客户会互相挤占,而分散在多个地址的调用方几乎不受约束。现在预算按密钥分级——密钥才是社区所有者签发、可轮换、并需要为之负责的那个东西。
| 分级 | 覆盖 | 10 秒 | 60 秒 |
|---|---|---|---|
| 读取 | 信息流、帖子、回复、空间、自省 | 120 | 600 |
| 媒体 | get_token 与 get_signed_urls | 30 | 120 |
| 写入 | 帖子的创建、修改与删除 | 20 | 120 |
| 搜索 | post/search | 20 | 60 |
| 成员查找 | member/lookup | 5 | 10 |
member/lookup 明显更严,是出于安全而非容量。它的其他约束都在限制「单次回答泄露多少」,只有这一条限制「能问多少次」——而后者才是把查询变成通讯录抓取的那一步。按它被设计的用法,即以人的节奏解析你已经持有的地址,每分钟十次绰绰有余。
- 超限返回 HTTP 429 与 code 10202,并带上 Retry-After 以及 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset 响应头。
- 请把这些数字当作起点而不是实测值——它们会依据观测到的流量调整。不要把它们写死进重试逻辑,认 429 与 Retry-After 即可。整套限流默认关闭,会先以观察模式运行一段时间再启用。
- 无论如何都请实现 429 退避,并把读取流量控制在大约每秒 10 次以内。
接收 Webhook
如果社区所有者配置了 Webhook 端点,或连接了 Slack、Discord、Zapier,投递体里会带两组含义不同的 ID。
| 字段 | 标识 | 跨重试 |
|---|---|---|
| event_id | 业务事件 | 不变 |
| occurred_at | 事件发生的时间 | 不变 |
| delivery_id | 本次投递尝试 | 每次都变 |
| timestamp | 本次尝试的发送时间 | 每次都变 |
请用 event_id 去重。你已经处理成功但响应太慢的那一次,重试到达时 delivery_id 是新的、event_id 不变。同一个事件同时投给自定义 Webhook 和 Zap 时,两边看到的 event_id 也相同,可以对齐。
投递是至少一次的,并且可能乱序。收到重复、以及收到迟到的旧事件,都是正常的;接收方需要自己做幂等。
event_id 可能不存在。在这个字段上线之前就已入队的任务不带它,投递体会整个省略该字段而不是传 0——传 0 会让所有旧任务看起来像同一个事件。处理时要能接受它缺失,缺失时退回按 delivery_id 尽力去重。
排障对照
真正会被报上来的现象,以及它们通常的原因。
| 现象 | 多半是 |
|---|---|
| 10007 invalid API key,但密钥是刚从后台复制的 | 密钥前后有空格或换行;或密钥已被吊销;或配了 IP 白名单而你的出口地址不在其中 |
| 22203 feature_not_available | 套餐的 API 权限是关的。所有者改完之后,套餐缓存最多需要 10 分钟才会过期 |
| 22204 quota_exceeded 且 limit 为 0 | 套餐的每日请求额度是 0,表示不可用——不是「没配所以无限」 |
| 在你以为能用的端点上收到 21304 | 密钥缺少那个 scope。scope 在创建时即固定,所以只能新建一把密钥 |
| 一把「全部权限」(*) 的老密钥收到 21304 | 残留的 * 不涵盖后来新增的 scope——media.read、spaces.read、members.read。重新签发一把并勾上它们 |
| 10202 加 HTTP 429 | 撞到限流。按 Retry-After 退避;若发生在 member/lookup,记得那个桶是每分钟十次 |
| 10005 加 schedule_cancel_unsupported | 你对普通帖传了 0 或更小的 scheduled_at。只有 blog 帖有草稿状态可以退回 |
| 原本能读能删的媒体令牌突然不行了 | 2026 年 9 月起,不带 scope 的令牌只有 upload 权限。请传 read 或 delete 的 scope |
| /member/lookup 返回 200 且 found 为 false | 这是正常的「没找到」,不是错误。命中与未命中都是 200——请按 found 判断 |
| 10005 invalid params 且提示 query | 搜索的字段名是 query,keyword 不是合法字段 |
| 20303 user not found | 该 id 不是本社区的成员,或者你传了数字 ID 而不是 hashid |
| 翻页翻不动了 | /feed/top 与 /space/feed/top 的游标是浮点数,被当字符串序列化之后就对不上了 |
| 媒体 URL 过一阵开始返回 403 | 签名 URL 有 TTL。请按需重新签发,不要落库保存 |
其他说明
- 社区由你的 API Key 反查得出。不要在路径或请求体里传 site id。
- scope 在密钥创建时即固定。要修改只能新建一把密钥并吊销旧的。
- 请把 API 密钥保存在服务端。放进客户端代码,等于让任何能读到它的人以你的成员身份发帖。
- OpenAPI 契约覆盖全部路径与 schema。有一处需要注意:生成它的工具不会输出请求体的 required,所以必填字段请以本页为准,而不是以 spec 为准。