同步回撥
同步回撥在使用者執行某些操作之前,先請求你的服務端,由你決定是否允許:放行、拒絕,或者改寫訊息的內容。例如只允許同一家公司的員工互相發訊息、給訊息補上業務欄位、只有付費使用者能加入某個群。
| 同步回撥 | 呼叫時機 | 你可以 | 被拒絕時操作方收到 |
|---|---|---|---|
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 中的人”。
