回调概述
回调是 IM 服务主动向你的业务服务端发出的 HTTP 请求,用于两类场景:
- 事件回调:应用内发生了变化(注册了用户、成为好友、群里加了人、发了一条消息、通话结束等)之后,把变化通知你的服务端。其中每条消息的抄送叫消息抄送。你可以据此同步业务数据、归档消息、统计通话时长。
- 同步回调:用户在客户端发消息、加好友、加群之前,先询问你的服务端是否允许。你的服务端可以放行、拒绝,或者改写消息的内容,例如只允许同一家公司的员工互相发消息、给付费群做准入检查。
你的服务端是接收方:在控制台配置接收回调的地址,按本节的说明实现一个 HTTPS 接口即可,不需要调用任何接口。本节的页面:
- 验证回调请求:签名算法、密钥轮换,以及 Python、Go、Node.js 的接收端示例代码;
- 事件回调:全部事件类型、触发时机和
data的字段,消息抄送的内容; - 同步回调:发消息前、加好友前、加群前等同步回调的请求、响应和失败时的处理。
在控制台中添加地址、选择事件、开启同步回调的步骤见回调配置。
两种回调
| 事件回调 | 同步回调 | |
|---|---|---|
| 发送时机 | 变化已经提交之后,通常 1 到 2 秒内 | 操作写入之前,操作方在等待你的响应 |
| 包括 | 用户、登录、在线状态、好友、群组、消息(消息抄送、撤回、编辑)、聊天室、音视频、内容安全、文件的变化,见事件回调 | 发消息前、发聊天室消息前、加好友前、加群前,见同步回调 |
| 你的响应 | 返回任意 2xx 表示收到,响应内容被忽略,不能撤销已经发生的变化 | 返回 2xx 和 JSON,决定放行、拒绝或改写 |
| 失败时 | 按退避间隔自动重试,最长 24 小时,之后可以手动重新投递 | 只请求一次,不重试;超时或出错时按你的设置放行或拒绝 |
| 可靠性 | 至少一次,可能重复,不保证顺序。在线状态、聊天室的消息抄送和撤回为尽力而为,见尽力而为的事件 | 每次操作请求一次;客户端重试同一个操作时会再次请求 |
| 订阅方式 | 每个回调地址选择要接收的事件 | 每种同步回调选择一个回调地址并开启 |
建议按以下顺序接入:
- 部署接收端:公网可以访问的 HTTPS 地址,先实现签名校验和下文的 URL 验证;
- 在控制台新建回调地址,保存只显示一次的签名密钥。保存后系统立即向地址发送一次 URL 验证,通过后地址变为“正常”;
- 用控制台的“测试发送”调通签名校验和事件的处理;
- 订阅需要的事件;需要同步回调的,先以“失败时放行”开启,在控制台观察一段时间的耗时和失败次数,确认接收端稳定后再按需要改为“失败时拒绝”。
回调地址
每个应用最多可以配置 5 个回调地址。每个地址有自己的签名密钥、订阅的事件、过滤条件和发送速度的设置,互不影响:同一个事件被两个地址订阅时,两个地址各自投递、各自重试。常见的用法是一个地址接收业务事件,一个地址接收量大的消息抄送并开启合并,同步回调再单独用一个响应快的地址。
| 设置 | 默认值 | 说明 |
|---|---|---|
| 名称 | — | 1 到 64 个字符,用于在控制台中区分地址 |
| URL | — | 接收回调的地址,规则见地址的要求 |
| 订阅的事件 | — | 1 到 50 项,写完整的事件类型或 group.* 这样的前缀,见订阅事件 |
| 过滤条件 | 不过滤 | 不接收由你的服务端引起的事件、不接收删除账号连带的事件等,见过滤条件 |
| 每批事件数 | 1 | 一次请求最多合并的事件数,1 到 100,见一次请求中的多个事件 |
| 超时 | 5000 毫秒 | 等待事件回调响应的时间,1000 到 10000 毫秒 |
| 并发上限 | 10 | 同时进行的事件回调请求数,1 到 50 |
| 每秒请求数上限 | 100 | 1 到 1000,不能超过应用的上限(默认每秒 500 个请求,全部地址合计,由平台设置) |
超过并发和速率上限的事件在队列中等待,只是晚一些发出,不会丢失。修改设置通常在 1 秒内生效,最迟 30 秒。
地址的状态
| 状态 | 含义 | 事件回调 | 同步回调 |
|---|---|---|---|
| 待验证 | 刚新建或刚修改了 URL,还没有通过 URL 验证 | 不生成投递,这期间的事件不会补发 | 不发请求,按失败处理 |
| 正常 | 通过了 URL 验证 | 正常投递 | 正常调用 |
| 已暂停 | 你在控制台暂停了它,适合接收端停机维护 | 照常排队、不发送,恢复后继续发送 | 照常调用 |
| 自动停用 | 持续失败 24 小时后被系统停用,见持续失败与自动停用 | 不生成新的投递,已排队的不再发送 | 照常调用 |
| 平台停用 | 地址被平台停用(通常是指向了不属于你的第三方),只有平台可以恢复 | 不生成新的投递 | 不发请求,按失败处理 |
地址的状态主要控制事件回调。同步回调绑定的地址处于“待验证”或“平台停用”时不发请求,按这种同步回调的失败策略处理,见失败时的处理;“已暂停”和“自动停用”不影响同步回调。
地址的要求
IM 服务替你向外发出请求,为了防止地址被用来探测平台的内网或攻击第三方,回调地址必须满足:
- 只允许
https,证书必须有效(不能是自签名证书、过期证书或与域名不符),TLS 1.2 以上; - 主机必须是公网的域名:不能是 IP 地址(包括十进制、十六进制等变体写法),也不能是
localhost、没有点的单个名称、以.local、.internal、.localhost结尾的名称,以及平台禁止的域名; - 端口只能是 443 或 1024 到 65535;
- 不能带用户名和密码(
user:pass@),不能带#片段;可以带查询参数;最长 1024 个字符; - 保存时域名解析出的全部 IP 都必须是公网地址;每次发送请求时,还会检查实际要连接的 IP,不是公网地址的放弃连接,记为失败(
blocked_address),因此域名在保存之后改为解析到内网地址也无法使用; - 不跟随重定向:返回
3xx一律记为失败(redirect)。
不允许的地址包括回环地址、私有网段(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)、链路本地地址(169.254.0.0/16,含云服务器的元数据地址)、运营商级 NAT(100.64.0.0/10)、组播、保留和文档网段,以及 IPv6 中对应的地址。接收端与你的其他服务在同一内网时,请通过公网的 HTTPS 地址(如反向代理、负载均衡)接收回调。
URL 不符合要求时,保存返回 400 invalid_argument,details.reason 为 url_not_allowed,details.rule 指出原因:
rule | 原因 |
|---|---|
scheme | 不是 https |
host | 主机是 IP 地址或内网名称,或 URL 格式不对 |
port | 端口不是 443 或 1024 到 65535 |
userinfo | 带了用户名和密码 |
fragment | 带了 # |
length | 超过 1024 个字符 |
denied_host | 平台禁止使用的域名 |
disabled_host | 这个主机名上有被平台停用的回调地址 |
private_address | 域名解析到了非公网地址 |
resolve_failed | 域名解析失败 |
masked_query | 查询参数的值是 ***,见下文 |
查询参数中的令牌:可以把接收端自己的令牌放在 URL 的查询参数中(如 ?token=xxx)。控制台、操作日志和平台中显示的 URL 会把每个查询参数的值替换为 ***,只保留参数名,所以修改 URL 时必须填写完整的新 URL,不能把显示的 URL 原样提交。
URL 验证
新建地址和修改 URL 后,地址处于“待验证”,系统立即向它发送一次 URL 验证请求,证明这个地址由你控制。地址必须在 5 秒内返回 2xx,响应体为 JSON 对象,其中 challenge 与请求中的值相同(可以有其他字段,最多读取 4 KB)。通过后地址变为“正常”,开始投递事件,也可以绑定同步回调。
请求示例(X-IM-Kind 为 verify):
POST /im/callback HTTP/1.1
Host: your-server.example.com
User-Agent: IM-Callback/1
Content-Type: application/json; charset=utf-8
X-IM-Kind: verify
X-IM-Request-Id: 100310966629040128
X-IM-Endpoint-Id: 100310966557736960
X-IM-Timestamp: 1791141598
X-IM-Nonce: c90a695d9294c5f757dd88350e0e3187
X-IM-Signature: v1=2e5cebe9c6c67af85d62c4cc0250579223acc3e89549eabf036d3c523cb08610
{"kind":"verify","request_id":"100310966629040128","app":"6195295143#doc-callback","endpoint_id":"100310966557736960","sent_at":"2026-10-04T19:19:58.132Z","challenge":"J9Kk_hKp_H74ZScznGSzU_-kL8b4r1MTACmzTFsNVP4"}你的接收端校验签名后返回:
{ "challenge": "J9Kk_hKp_H74ZScznGSzU_-kL8b4r1MTACmzTFsNVP4" }challenge 每次都不同。验证失败时(超时、连接失败、返回非 2xx、challenge 不对),地址保持“待验证”,修好接收端后在控制台点击“验证”重新发送。URL 验证和测试发送每个应用合计每分钟最多 20 次,新建地址、修改 URL 后自动发起的验证也计入;额度用完时地址照常保存,只是不发送验证,稍后再点击“验证”即可。
出口 IP
平台可以让回调请求从固定的出口 IP 发出,控制台的“回调”页会列出这些 IP,你可以在接收端的防火墙上只允许它们访问。没有列出出口 IP 时,请求的来源 IP 不固定,请只依靠签名确认请求的来源。
请求格式
全部回调都是 POST 请求,请求体为 UTF-8 编码的 JSON。
请求头
| 请求头 | 说明 |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | IM-Callback/1 |
X-IM-Kind | 请求的种类:event 事件回调,hook 同步回调,verify URL 验证 |
X-IM-Hook | 同步回调的类型,如 message.before_send,只在 X-IM-Kind 为 hook 时有 |
X-IM-Request-Id | 请求 ID,每次发送都不同(重试也不同),与控制台请求记录中的相同,用于排查问题 |
X-IM-Endpoint-Id | 回调地址的 ID |
X-IM-Timestamp | 发送时的 Unix 时间,单位为秒 |
X-IM-Nonce | 随机数,32 个十六进制字符,每次发送都不同 |
X-IM-Signature | 签名,v1= 加 64 个十六进制字符;轮换密钥的宽限期内为两个,用逗号分隔,见签名算法 |
HTTP 请求头不区分大小写,有的框架会把它们显示为 X-Im-Kind 或 x-im-kind。请求的种类、请求 ID 和地址 ID 在请求体中也有,受签名保护;请求头中的副本只用于不解析请求体就能分流,业务判断请以请求体为准。
请求体
三种请求的请求体都有以下公共字段:
| 字段 | 类型 | 说明 |
|---|---|---|
kind | String | event、hook 或 verify,与请求头 X-IM-Kind 相同 |
request_id | String | 请求 ID,与请求头 X-IM-Request-Id 相同 |
app | String | 应用的 AppKey,如 6195295143#demo。多个应用共用一个接收端时据此区分 |
endpoint_id | String | 回调地址的 ID |
sent_at | String | 发送时间,见时间格式 |
事件回调另有 events 数组,同步回调另有 hook、timeout_ms、test、data(见同步回调),URL 验证另有 challenge。
事件回调的请求示例,有人加入了群:
POST /im/callback HTTP/1.1
Host: your-server.example.com
User-Agent: IM-Callback/1
Content-Type: application/json; charset=utf-8
X-IM-Kind: event
X-IM-Request-Id: 100311905645625344
X-IM-Endpoint-Id: 100310966557736960
X-IM-Timestamp: 1791141822
X-IM-Nonce: 36e818ae8f4cd9fdaa24db6ca521475d
X-IM-Signature: v1=f7be9e46c15142401297376147fdea08c762c6ca7364...{
"kind": "event",
"request_id": "100311905645625344",
"app": "6195295143#demo",
"endpoint_id": "100310966557736960",
"sent_at": "2026-10-04T19:23:42.011Z",
"events": [
{
"id": "100311903162597376",
"type": "group.members_added",
"occurred_at": "2026-10-04T19:23:41.419Z",
"attempt": 1,
"test": false,
"data": {
"origin": "client",
"group_id": "100311903108071424",
"member_version": 2,
"member_count": 2,
"members": [
{
"username": "carol",
"role": "member",
"joined_via": "create",
"inviter": "alice",
"joined_at": "2026-10-04T19:23:41.409Z"
}
],
"operator": "alice"
}
}
]
}events 中每一项是一个事件:
| 字段 | 类型 | 说明 |
|---|---|---|
id | String | 事件 ID。同一个事件的每次重试、重新投递都相同;一个事件被多个地址订阅时,每个地址收到的事件 ID 也相同。按它去重 |
type | String | 事件类型,如 group.members_added,见事件一览 |
occurred_at | String | 事件发生的时间 |
attempt | Number | 这个事件是第几次发送,从 1 开始;重新投递后从 1 重新计数 |
test | Boolean | 是否为控制台的测试发送。测试事件使用示例数据,接收端可以据此不执行业务操作 |
data | Object | 事件的内容,字段因事件类型而不同,见事件回调 |
一次请求中的多个事件
不论是否合并,事件都放在 events 数组中。地址的“每批事件数”大于 1 时,一次请求最多带这么多个已经到期的事件,请求体不超过 1 MB;同一批中的事件不保证按发生的先后排列。合并的请求整体成功或整体失败:返回非 2xx 时,这一批的全部事件都会重试。第 3 次及之后的发送不再合并,每个事件单独发送,避免一个处理出错的事件拖住同批的其他事件。
消息抄送这类量大的事件建议开启合并。例如一个每分钟 3 万条消息的应用,每秒约 500 个事件,每批 100 个时每秒只需约 5 个请求。
data 的写法
- 用户用用户名表示,不出现内部的用户 ID;已删除的用户为
null。 - ID 都是字符串,如
group_id、message_id、call_id。 - 时间为固定三位毫秒的 UTC 时间,如
2026-10-04T19:23:41.409Z。例外是登录会话的时间(user.session_created的created_at、user.sessions_revoked的revoked_before、user.relogin_required的sessions_revoked_before),为六位微秒,如2026-10-04T19:21:11.735215Z,比较先后时不要截断。 - 消息的
body、ext按原样放入,格式见消息格式。 - 以后会增加字段和事件类型,接收端必须忽略不认识的字段和事件类型,不要因此返回错误。
- 大小:一个事件的
data不超过 70 KB。万一超出,会去掉其中的内容和列表类的字段(如body、ext、members),只保留 ID、用户名、类型、状态、原因、版本号和时间类的字段,并加上"truncated": true。收到这样的事件时,请按其中的 ID 用 OpenAPI 查询当前的状态。
origin:事件由谁引起
每个事件的 data 都以 origin 开头,说明这次变化由谁引起:
origin | 含义 |
|---|---|
client | 用户在客户端的操作,如用户自己发消息、加好友、退群 |
server | 你的服务端(OpenAPI)或你在控制台的操作,以及你的审核人员作出结论的内容安全处置 |
platform | 平台管理员的操作,如平台屏蔽了文件、封禁了用户 |
system | 系统自动执行的操作,如删除账号后清理他的好友和群成员身份、内容安全的自动处置、通话结束后写入的通话记录消息 |
null | 只有 presence.changed(在线状态)没有来源 |
由你的服务端引起的变化,结果你本来就知道。可以为地址开启过滤条件“不接收由租户服务端引起的事件”,不再接收 origin 为 server 的事件(撤回事件除外),见过滤条件。
响应
| 请求 | 成功的条件 | 读取的响应体 |
|---|---|---|
| 事件回调 | 在地址的超时时间(默认 5 秒)之内返回任意 2xx,这一批事件都算投递成功 | 响应体被忽略,最多读取 16 KB |
| URL 验证 | 5 秒之内返回 2xx,响应体中有正确的 challenge | 最多 4 KB,超出算失败 |
| 同步回调 | 在等待时间之内返回 2xx 和合法的 JSON,见同步回调的响应 | 最多 64 KB,超出算失败 |
其他情况都算失败:返回 3xx(不跟随重定向)、4xx、5xx,超时,连接失败,TLS 握手失败。签名校验不通过时,建议返回 401。
接收事件回调时请先校验签名、按事件 ID 去重并保存事件,立即返回 2xx,再在你自己的队列中异步处理。在回调请求中执行耗时的业务逻辑会让请求超时,事件被当作失败重发,接收端的压力随之变大。
投递与重试
重试
事件回调失败后按以下间隔重试,每次的间隔随机增减 20%,避免大量事件同时重试:
| 第几次失败后 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 等待 | 10 秒 | 30 秒 | 1 分钟 | 5 分钟 | 15 分钟 | 30 分钟 | 1 小时 | 2 小时 | 4 小时 | 8 小时 | 8 小时 |
- 接收端返回
429或503并带Retry-After响应头(秒数或 HTTP 日期)时,取它(最长 1 小时)与上表的间隔中较大的一个。接收端维护时可以返回503和Retry-After,让事件晚一些再来。 - 事件从排队起 24 小时内仍未成功的转为投递失败,最多发送 12 次。投递失败的事件保留 7 天,可以在控制台重新投递。
- 每次重试都是新的请求:请求 ID、时间戳、随机数和签名都重新生成,事件 ID 不变,
attempt加 1。
熔断:一个地址连续失败 5 次后暂停发送 30 秒,之后先放行一个请求试探:试探成功则恢复,失败则暂停的时间加倍,最长 5 分钟。暂停期间事件照常排队,不计入发送次数。接收端整体故障时,不会被大量注定失败的请求持续冲击。
重复与乱序
- 至少一次:同一个事件可能收到不止一次(例如接收端其实已经处理、但没有在超时之内返回)。请按事件
id去重,去重记录至少保留 24 小时;需要重新投递几天前失败的事件时,保留 7 天更稳妥。 - 不保证顺序:重试中的事件可能晚于之后发生的事件到达,同一批中的事件也不按先后排列。请以事件中的版本号(如
friend.added的version、group.members_added的member_version、presence.changed的version)和occurred_at判断新旧,旧事件不要覆盖新状态。 - 事件是发生时的快照:例如
friend.added中的昵称是成为好友那一刻的,之后修改了昵称不会更新已经排队的事件。需要当前状态时用 OpenAPI 查询。
尽力而为的事件
在线状态(presence.changed)、聊天室的消息抄送(chatroom.message_sent)和撤回(chatroom.message_recalled)量很大,先在服务端的内存中攒约 1 秒再批量写入投递队列。服务实例在这期间崩溃或缓冲已满时,这些事件会丢失,不会补发。写入投递队列之后的投递、重试与其他事件相同。需要准确的状态时,请用 OpenAPI 查询(如在线状态)。
积压上限
每个地址待投递的事件最多 50 万条。接收端长时间不可用、积压达到上限后,新的事件不再排队,直接丢弃,控制台的统计中计入“丢弃”,并通知租户的 owner 和 admin(每 24 小时最多一次)。一个每秒 500 个事件的地址宕机约 17 分钟就会积满,量大的地址请开启合并、提高并发。丢失的消息抄送可以用导出应用的消息补齐。
持续失败与自动停用
- 一个地址从最近一次成功之后开始失败,持续 1 小时仍没有一次成功时,系统通知租户的
owner和全部admin(有已验证邮箱的发邮件,否则发短信),提示 24 小时后将自动停用; - 持续 24 小时仍没有一次成功、且最近 1 小时内仍有失败的请求时,地址自动停用:不再为它生成新的投递,已排队的不再发送(到期后转为投递失败),并再次通知。
- 只有事件回调的失败计入;同步回调、URL 验证和测试发送的失败不会让地址被提醒或停用。暂停期间不发送,不算作失败。
修好接收端后,在控制台点击“恢复”:系统先发送一次 URL 验证,通过后地址恢复正常,再重新投递停用前投递失败的事件。停用期间发生的事件没有排队,不会补发,需要时用 OpenAPI 或导出应用的消息补齐。
投递记录与重新投递
控制台的回调地址详情中可以查看:
| 记录 | 内容 | 保留 |
|---|---|---|
| 投递队列 | 每个事件对这个地址的投递:状态(待投递、已投递、失败)、发送次数、最近的错误和状态码;可以按事件类型筛选、按事件 ID 查询,查看事件的 data(消息类事件的 body、ext 不显示) | 投递成功的 1 小时,投递失败的 7 天 |
| 请求记录 | 每次发出的请求:种类、结果、状态码、耗时、对方的 IP,失败时还有错误原因和响应的开头部分。URL 验证、测试发送、重新投递和失败的请求基本全部记录,成功的请求按抽样记录;不记录事件内容 | 7 天 |
| 统计 | 按小时统计的请求数、失败数、超时数、耗时、事件从发生到投递成功的延迟,每种事件的排队、成功、失败、丢弃数 | 90 天 |
重新投递:投递失败的事件可以在控制台逐个,或按失败的时间段(最长 7 天,可以只选某些事件类型)批量重新投递,每次最多 1 万个,没处理完的再提交一次。重新投递的事件回到待投递,发送次数清零,24 小时的期限重新计算。地址必须是“正常”或“已暂停”(暂停时排队,恢复后发送);自动停用的地址要先恢复。
消息内容的保留:投递成功后,投递队列中的消息内容随即清除;投递失败的消息类事件,24 小时后清除其中的 body、ext。之后重新投递时,系统按消息 ID 查询消息的当前内容:消息已被撤回、擦除或已过期的,事件中的 body、ext 为 null,content_available 为 false。聊天室的消息不长期保存,清除之后无法再查到内容。
不回调的情况
- 租户被暂停(包括欠费暂停)、处于注销冷静期,应用被停用或被平台暂停服务期间,不生成新的投递,这期间发生的事件不回调,恢复后也不补发;已经排队的暂停发送,恢复后继续,排队超过 24 小时的转为投递失败。应用处于只读状态时照常回调。见应用和租户的状态。
- 删除回调地址后立即停止投递,排队的事件和投递记录随后删除,不能恢复。
- 应用删除、租户注销后停止全部回调。
频道回调
独立频道新增 9 种 rtc_channel.* 事件,覆盖频道创建 / 更新 / 关闭、会话加入 / 退出与封禁变化。它们使用本页相同的签名、重试、合并与去重机制,字段见频道事件。票据授权在业务服务端完成,没有新增“加入频道前”的同步回调。
