同步回调
同步回调在用户执行某些操作之前,先请求你的服务端,由你决定是否允许:放行、拒绝,或者改写消息的内容。例如只允许同一家公司的员工互相发消息、给消息补上业务字段、只有付费用户能加入某个群。
| 同步回调 | 调用时机 | 你可以 | 被拒绝时操作方收到 |
|---|---|---|---|
message.before_send 发消息前 | 发送单聊、群聊消息之前;可以设置是否包括服务端发送的消息和服务端的编辑 | 放行、拒绝、改写 body 和 ext | 403 message_rejected |
chatroom.before_send 发聊天室消息前 | 发送聊天室消息之前 | 放行、拒绝、改写 body 和 ext | message_rejected |
friend.before_add 加好友前 | 用户发送好友申请之前;可以设置同意好友申请时也调用 | 放行、拒绝 | 403 permission_denied |
group.before_join 加群前 | 用户建群时带初始成员、邀请他人、申请加入公开群、通过入群链接加入之前 | 放行、全部拒绝,邀请和建群时还可以只拒绝其中几个人 | 403 permission_denied,邀请和建群时按人返回 |
每种同步回调在控制台单独开启,选择一个已通过验证的回调地址,见回调配置。默认都不开启。目前没有登录前、注册前、进入聊天室前、发起通话前的同步回调。
同步回调的耗时直接加在用户的操作上
用户发消息时要等你的服务端响应之后才能发出,你的响应时间会直接加在每条消息的发送耗时上。请让接收端在 200 毫秒内响应,聊天室的回调更要就近部署;接收端故障时的处理见失败时的处理。
调用规则
- 只针对客户端的操作:你的服务端(OpenAPI)和控制台执行的操作是你自己的决定,不调用同步回调。例外是
message.before_send和chatroom.before_send,可以在调用条件vias中加上openapi、console,让服务端和控制台发送的消息也经过检查(多个业务系统都调用 OpenAPI 发消息、需要统一检查时使用)。 - 调用条件:每种同步回调可以限定在哪些情况下调用,如只对群聊、只对
custom类型的消息、只对申请加入和入群链接。不满足条件的操作直接放行,不发请求。各类型的条件见下文。 - 调用的位置:同步回调排在操作自身的参数校验、权限检查、频率限制和内容安全检查之后:被这些检查拒绝的操作不会调用同步回调;内容安全替换了文字的,你收到的是替换之后的内容;你改写的内容不再经过内容安全检查。加好友和加群的部分规则(如是否已经是好友、对方的加好友方式、群人数上限)在同步回调之后检查,所以同步回调放行的操作仍可能因为这些规则失败。
- 只请求一次:同步回调不重试。客户端因超时重新提交同一个操作时,会再次请求,可以用消息的
client_msg_id、好友申请的双方、入群的群和用户识别重复的请求。 - 之后的步骤不再调用:同步回调只在发起时调用一次。例如入群申请在申请时调用,群主审批同意时不再调用;邀请在邀请时调用,被邀请人接受邀请时不再调用。
请求
同步回调的请求头 X-IM-Kind 为 hook,X-IM-Hook 为同步回调的类型,签名与其他回调相同,见验证回调请求。请求体:
| 字段 | 类型 | 说明 |
|---|---|---|
kind | String | 固定为 hook |
hook | String | 同步回调的类型,如 message.before_send |
request_id | String | 请求 ID,每次请求都不同 |
app | String | 应用的 AppKey |
endpoint_id | String | 回调地址的 ID |
sent_at | String | 发送时间 |
timeout_ms | Number | 这次实际等待你响应的毫秒数,超过后按失败处理,见等待时间。来不及时可以直接放弃处理 |
test | Boolean | 是否为控制台的测试请求。测试请求使用示例数据 |
data | Object | 这次操作的内容,字段因类型而不同,见下文各节 |
POST /im/hooks HTTP/1.1
Host: your-server.example.com
User-Agent: IM-Callback/1
Content-Type: application/json; charset=utf-8
X-IM-Kind: hook
X-IM-Hook: message.before_send
X-IM-Request-Id: 100312172998950912
X-IM-Endpoint-Id: 100311572261371904
X-IM-Timestamp: 1791141885
X-IM-Nonce: 8ef375653bb4e8789852845175ef5e07
X-IM-Signature: v1=af0170256ec0cc4761d702973722e2272984d1fe517e...{
"kind": "hook",
"hook": "message.before_send",
"request_id": "100312172998950912",
"app": "6195295143#demo",
"endpoint_id": "100311572261371904",
"sent_at": "2026-10-04T19:24:45.753Z",
"timeout_ms": 800,
"test": false,
"data": {
"via": "client",
"client_msg_id": "c-0001",
"conversation_type": "single",
"sender_username": "alice",
"recipient_username": "carol",
"recipient_usernames": null,
"group_id": null,
"type": "text",
"body": { "text": "明天上午十点评审" },
"ext": null,
"mention_usernames": [],
"mention_all": false,
"reply_seq": null,
"need_receipt": false,
"online_only": false,
"edit": null
}
}响应
在等待时间之内返回 HTTP 2xx,响应体为 JSON 对象,最大 64 KB。action 决定结果:
action | 其他字段 | 适用于 |
|---|---|---|
allow 放行 | — | 全部 |
reject 拒绝 | reason:可选,你的原因代码,1 到 64 个小写字母、数字、_、.、-;message:可选,给用户看的提示,最长 256 个字符,不能有换行等控制字符 | 全部 |
modify 改写 | body:新的消息体,JSON 对象;ext:新的扩展字段,JSON 对象或 null(去掉扩展字段)。至少给出一个,没有给出的保持原样 | message.before_send、chatroom.before_send |
partial 部分拒绝 | rejected:被拒绝的人,至少一项,每项为 username(必须是请求的 targets 中的人)和可选的 reason、message,规则同 reject | group.before_join 的邀请和建群 |
{ "action": "allow" }{ "action": "reject", "reason": "forbidden_word", "message": "消息中有不允许的内容,请修改后再发" }- 不认识的其他字段被忽略。
- 返回的不是
2xx,或者是2xx但响应体不是 JSON 对象、action不认识或不适用于这种同步回调、字段不符合上表的,都按失败处理,见下文。例如对friend.before_add返回modify、对申请加入返回partial,都按失败处理。 - 响应不需要签名,响应的真实性由 HTTPS 保证。
等待时间
每种同步回调可以设置等待时间,100 到 3000 毫秒,默认 1000 毫秒。实际等待的时间还受操作方的总时限约束:
- 消息:发消息前的全部检查(内容安全和发消息前回调)合计最多 3 秒;
- 聊天室消息:聊天室的消息要在几百毫秒内出现在全部观众面前,全部检查合计最多 500 毫秒。
内容安全先执行,用掉一部分时间;同步回调实际等待的时间是你的设置与剩余时间减去 50 毫秒中较小的一个,即请求体中的 timeout_ms。剩余不足 100 毫秒时不发请求,直接按失败处理。
失败时的处理
以下情况都算失败:
- 超时、连接失败、TLS 握手失败、返回非
2xx(包括3xx)、响应体超过 64 KB 或不符合响应的要求; - 绑定的回调地址处于“待验证”(如刚修改了 URL)或“平台停用”,不发请求;
- 熔断中或并发已满,不发请求(见下文);
- 剩余的时间不足 100 毫秒,不发请求。
失败时按这种同步回调的失败策略(on_failure)处理,每种同步回调单独设置:
| 失败策略 | 处理 | 适用的场景 |
|---|---|---|
allow 放行(默认) | 操作照常执行,就像没有开启同步回调 | 同步回调用于改善体验,接收端故障时不应影响用户发消息、加好友 |
reject 拒绝 | 操作被拒绝,details.reason 为 callback_unavailable,与你主动拒绝(app_rejected)区分开 | 必须经过检查才能执行的操作。代价是接收端故障时,全应用的这类操作都会失败 |
先用放行开启
建议先以“放行”开启,在控制台的同步回调统计中观察一段时间的耗时和失败次数,确认接收端稳定后再按需要改为“拒绝”。修改绑定地址的 URL 会让地址回到“待验证”,验证通过之前,失败策略为“拒绝”的同步回调会拒绝全部相应的操作,请先改绑到其他地址或暂时改为放行。
熔断:每个服务实例对每个地址的同步回调分别统计,最近 10 秒内不少于 20 次请求且超过一半失败,或连续 5 次超时时,熔断 10 秒:这期间不发请求,直接按失败处理;10 秒后先放行一个请求试探,成功则恢复。接收端整体故障时,每次操作不会都等满超时时间。
并发:每个服务实例对同一个地址同时最多 200 个同步回调请求,超出的不等待,直接按失败处理。
地址的状态:绑定的地址被你暂停或被自动停用时,同步回调照常调用,这两种状态只影响事件回调。删除地址时,绑定它的同步回调随之关闭。
控制台的“请求记录”中可以查看每次同步回调的结果(allow、reject、modify、partial,失败后按策略处理的为 fallback_allow、fallback_reject)、耗时和失败的原因,例如响应无效时的记录:
{
"request_id": "100313774824620032",
"created_at": "2026-10-04T19:31:07.660Z",
"kind": "hook",
"name": "message.before_send",
"outcome": "failed",
"verdict": "fallback_reject",
"http_status": 200,
"error_kind": "invalid_response",
"error_detail": "不认识的 action",
"latency_ms": 1,
"response_excerpt": "{\"action\": \"maybe\"}"
}操作方收到的错误
你拒绝,或者失败后按“拒绝”策略处理时,发起操作的客户端(或调用 OpenAPI 的你的服务端)收到错误:
| 同步回调 | 错误 | message | details |
|---|---|---|---|
message.before_send、chatroom.before_send | 403 message_rejected | 你给出的 message;没有给出时,以及失败后拒绝时为“消息发送失败” | reason:app_rejected 你拒绝,callback_unavailable 失败后按策略拒绝;app_reason:你给出的 reason,没有时为 null |
friend.before_add、group.before_join | 403 permission_denied | 你给出的 message;没有给出时,以及失败后拒绝时为“操作被拒绝” | 同上 |
你拒绝时,客户端收到:
{
"error": {
"code": "message_rejected",
"message": "消息中有不允许的内容,请修改后再发",
"details": {
"app_reason": "forbidden_word",
"reason": "app_rejected"
},
"request_id": "IOUGZD4RGKHRMUJ2PYOPHH3DBM"
}
}接收端超时,失败策略为“拒绝”时:
{
"error": {
"code": "message_rejected",
"message": "消息发送失败",
"details": {
"app_reason": null,
"reason": "callback_unavailable"
},
"request_id": "KX2HPS7QFS7ZBSUDKVBG7FZWHE"
}
}客户端可以按 details.reason 区分:app_rejected 时直接展示 message;callback_unavailable 时提示“服务繁忙,请稍后重试”。内容安全拒绝消息时同样返回 message_rejected,details.reason 为 sensitive_content 等,见内容安全。
批量操作中,拒绝作为每一项的结果返回,整个请求仍然成功:服务端批量发送消息时,同步回调只调用一次,结果对全部接收人生效;客户端邀请多人、建群带初始成员时,被拒绝的人在 results 中逐个返回 permission_denied,其余照常入群,见 group.before_join。
message.before_send
发送单聊、群聊消息之前调用。改写后的消息按正常的规则重新校验格式和大小,不合格的拒绝发送,发送者收到 400 invalid_argument 或 413 payload_too_large;不能改变消息的类型。
调用条件:
| 条件 | 默认 | 说明 |
|---|---|---|
vias | ["client"] | 对哪些入口发送的消息调用:client 客户端,openapi 你的服务端,console 控制台 |
conversation_types | ["single", "group"] | 会话类型:single 单聊,group 群聊 |
message_types | 全部 | 只对这些类型的消息调用,如 ["text", "custom"],最多 20 项 |
include_online_only | false | 是否对只推在线的消息调用 |
include_edits | false | 是否对消息的编辑调用。编辑只能由服务端和控制台发起,所以 vias 中还要有 openapi 或 console |
data 的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
via | String | 发送的入口:client、openapi、console |
client_msg_id | String | 消息的去重 ID。服务端发送时没有给出的,为服务端生成、随后写入的那个 |
conversation_type | String | single 或 group |
sender_username | String | 发送者;以系统身份发送的群消息为 null |
recipient_username | String | 单聊的接收者;群聊,以及服务端批量发送给多人时为 null |
recipient_usernames | Array<String> | 服务端批量发送给多人时的全部接收者(最多 1000 个);其他情况为 null |
group_id | String | 群聊的群 ID;单聊为 null |
type | String | 消息类型 |
body | Object | 消息体,经过内容安全替换之后的内容 |
ext | Object | 扩展字段,没有时为 null |
mention_usernames | Array<String> | 被 @ 的成员,没有时为空数组 |
mention_all | Boolean | 是否 @ 全体成员 |
reply_seq | Number | 引用的消息的序号,没有引用时为 null |
need_receipt | Boolean | 是否要求群已读回执 |
online_only | Boolean | 是否为只推在线的消息 |
edit | Object | 编辑消息时为被编辑的消息:message_id 和 seq;发送时为 null |
请求示例见上文的请求。把消息中的词替换掉、并补上扩展字段:
{
"action": "modify",
"body": { "text": "请**这个词 @卡罗尔" },
"ext": { "checked": true }
}发送者收到的消息和对方收到的都是改写后的内容,消息抄送中也是改写后的内容。
chatroom.before_send
发送聊天室消息之前调用,全部检查合计最多 500 毫秒,见等待时间。可以改写 body 和 ext,不能改变消息的类型和优先级。
调用条件:
| 条件 | 默认 | 说明 |
|---|---|---|
vias | ["client"] | 同 message.before_send |
message_types | 全部 | 同 message.before_send |
priorities | 全部 | 只对这些优先级的消息调用:high、normal、low |
data 的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
via | String | 发送的入口:client、openapi、console |
client_msg_id | String | 消息的去重 ID |
room_id | String | 聊天室 ID |
sender | String | 发送者;以系统身份发送时为 null |
type | String | 消息类型 |
body | Object | 消息体,经过内容安全替换之后的内容 |
ext | Object | 扩展字段,没有时为 null |
priority | String | 消息的优先级:high、normal、low |
{
"kind": "hook",
"hook": "chatroom.before_send",
"request_id": "100312464121397248",
"app": "6195295143#demo",
"endpoint_id": "100311572261371904",
"sent_at": "2026-10-04T19:25:55.162Z",
"timeout_ms": 300,
"test": false,
"data": {
"via": "client",
"client_msg_id": "r-0001",
"room_id": "100312409813549056",
"sender": "bob",
"type": "text",
"body": { "text": "大家好" },
"ext": null,
"priority": "normal"
}
}被拒绝时,客户端在长连接上收到的 chatroom.send 的回复:
{
"type": "reply",
"id": "s1",
"error": {
"code": "message_rejected",
"message": "消息中有不允许的内容,请修改后再发",
"details": { "app_reason": "forbidden_word", "reason": "app_rejected" },
"request_id": "QODIXCVEJUAUFLC5ZHRCGBFYIO"
}
}改写时,回复中带改写后的 body 和 ext,客户端据此更新本地显示的消息。
friend.before_add
用户在客户端发送好友申请之前调用,包括会被直接通过的申请(对方允许任何人添加时,这是唯一的检查机会)。可以设置同意好友申请时也调用,用于判断条件可能在申请之后发生变化的场景(如双方已不在同一个组织)。只能放行或拒绝。
调用条件:
| 条件 | 默认 | 说明 |
|---|---|---|
stages | ["request"] | 在哪些时机调用:request 发送申请时,accept 同意申请时 |
data 的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
stage | String | request 发送申请,accept 同意申请 |
from_username | String | 申请人。同意申请时也是申请人,不是同意的人 |
to_username | String | 被申请人 |
message | String | 附言:发送时为经过内容安全检查后的附言,同意时为申请中保存的附言 |
add_source | String | 添加来源,没有时为空字符串 |
{
"kind": "hook",
"hook": "friend.before_add",
"request_id": "100311656000651264",
"app": "6195295143#demo",
"endpoint_id": "100311572261371904",
"sent_at": "2026-10-04T19:22:42.491Z",
"timeout_ms": 1000,
"test": false,
"data": {
"stage": "request",
"from_username": "carol",
"to_username": "vip",
"message": "你好",
"add_source": "qrcode"
}
}拒绝时:
{ "action": "reject", "reason": "vip_protected", "message": "对方暂不接受好友申请" }申请人收到:
{
"error": {
"code": "permission_denied",
"message": "对方暂不接受好友申请",
"details": {
"app_reason": "vip_protected",
"reason": "app_rejected"
},
"request_id": "FM4SQXYNG6EU4LS3OVDM736SBN"
}
}被拒绝的申请不会写入,对方不会收到申请。同意申请时被拒绝的,申请仍保持待处理。
group.before_join
用户在客户端建群时带初始成员、邀请他人、申请加入公开群、通过入群链接加入之前调用。建群时没有初始成员的不调用;申请加入私有群、链接无效等由群组自己的规则拒绝的情况,不调用。
调用条件:
| 条件 | 默认 | 说明 |
|---|---|---|
joined_vias | 全部 | 对哪些入群方式调用:create 建群带初始成员,invite 邀请,apply 申请加入,link 入群链接 |
group_types | 全部 | 对哪些群调用:private 私有群,public 公开群 |
data 的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
joined_via | String | 入群方式:create、invite、apply、link,与 group.members_added 中成员的 joined_via 取值相同 |
group | Object | 群:group_id(建群时为 null)、type、name |
operator | String | 发起的用户:建群的人、邀请人、申请人、使用入群链接的人 |
targets | Array<String> | 要入群的人。申请和入群链接时为发起人本人;邀请和建群时为被邀请的人,最多 100 个 |
message | String | 申请理由或邀请附言,经过内容安全检查后的内容;没有时为空字符串 |
link_creator | String | 通过入群链接加入时,生成链接的人;其他为 null |
建群时带了两个初始成员:
{
"kind": "hook",
"hook": "group.before_join",
"request_id": "100311903112265728",
"app": "6195295143#demo",
"endpoint_id": "100311572261371904",
"sent_at": "2026-10-04T19:23:41.407Z",
"timeout_ms": 1000,
"test": false,
"data": {
"joined_via": "create",
"group": { "group_id": null, "name": "项目组", "type": "private" },
"operator": "alice",
"targets": ["carol", "dave"],
"message": "一起来讨论项目",
"link_creator": null
}
}只拒绝其中一个人:
{
"action": "partial",
"rejected": [
{ "username": "dave", "reason": "not_verified", "message": "对方还没有完成实名认证" }
]
}群照常创建,carol 入群,建群的响应中 dave 的结果为 permission_denied:
{
"group_id": "100311903108071424",
"...": "...",
"results": [
{ "username": "carol", "status": "joined" },
{
"username": "dave",
"code": "permission_denied",
"message": "对方还没有完成实名认证",
"details": { "app_reason": "not_verified", "reason": "app_rejected" }
}
]
}邀请和建群时返回 reject,全部被邀请的人都被拒绝,群照常创建(只有群主)。申请加入和通过入群链接加入时只能返回 allow 或 reject,被拒绝时整个请求返回 403 permission_denied:
{
"error": {
"code": "permission_denied",
"message": "只有本公司员工可以加入",
"details": {
"app_reason": "not_member_of_org",
"reason": "app_rejected"
},
"request_id": "VIAXZQTJITEAQHMNA6Z3M6OKYV"
}
}测试同步回调
在控制台的“同步回调”中点击某种同步回调的“测试”,系统用示例数据(test 为 true)向绑定的地址发送一次请求,不要求地址已通过 URL 验证,显示解析出的结果,或者响应哪里不符合要求。测试请求与 URL 验证、事件的测试发送共用每个应用每分钟 20 次的额度。测试结果示例:
{
"ok": true,
"request_id": "100313736555790336",
"http_status": 200,
"latency_ms": 1,
"remote_ip": "203.0.113.10",
"error_kind": null,
"error_detail": null,
"response_excerpt": "{\"action\": \"allow\"}",
"status": "active",
"verdict": "allow"
}响应不符合要求时,ok 为 false,error_kind 为 invalid_response,invalid_reason 说明原因,如“不认识的 action”“rejected 中有不在 targets 中的人”。
