发送消息
你的业务服务端可以以任意用户的身份发送单聊或群聊消息,也可以以系统的身份发送群消息(没有发送者,显示为群里的系统通知),还可以以同一个发送者给一批用户各发一条单聊消息。服务端发送的通常是系统通知、订单状态、客服消息这类由你的业务决定能否发送的消息。消息的类型和内容格式见消息格式。
消息写入后立即推送给接收者的全部在线设备,离线的设备在下次登录同步时收到;App 不在前台的设备还会收到离线推送,可以用 push 字段指定通知的标题和内容、关闭推送或强制推送。消息的 via 为 openapi,客户端可以据此区分服务端发送的消息和用户自己发送的消息。
用 client_msg_id 防止重复发送
网络超时后,你无法确定上一次发送是否已经成功。请为每条消息生成一个唯一的 client_msg_id(如业务单号或 UUID),重试时原样带上:同一发送者在同一会话中相同的 client_msg_id 只写入一次,重试返回第一次写入的消息,不会产生重复消息。
服务端发送的规则
服务端发送时,由你的业务决定谁能给谁发消息,服务端只做以下检查:
| 检查 | 不通过时 |
|---|---|
发送者(from)存在且未被删除 | 404 not_found |
| 发送者没有被平台封禁。被你的应用封禁的用户仍可以作为发送者 | 403 user_disabled |
| 单聊的接收者存在且未被删除,不是发送者本人 | 404 not_found、400 invalid_argument |
| 群存在且未解散,没有被平台封禁。被你的应用封禁的群仍可以发送(如通知成员封禁的原因) | 404 not_found、403 group_disabled |
| 指定了发送者的群消息,发送者是群成员 | 403 not_group_member |
| 消息的类型、内容和大小符合消息格式的要求 | 400 invalid_argument、413 payload_too_large |
| 按群、按应用的发送限流(见下文) | 429 rate_limited |
| 内容安全的平台规则;在控制台为消息打开“服务端提交的内容也检查”后,也按你的词库和第三方审核检查,见服务端提交的内容 | 403 message_rejected |
发消息前回调:只在它的调用条件 vias 包含 openapi 时调用,排在内容安全之后。你的回调接收端拒绝,或回调失败后按“拒绝”策略处理时不发送;改写了内容的按改写后的内容发送,改写后的内容同样要符合消息格式和大小的要求 | 403 message_rejected;400 invalid_argument、413 payload_too_large |
以下检查只对用户在客户端发送的消息生效,服务端发送时不检查(内容安全和发消息前回调不在此列,按上表的规则检查;发消息前回调默认也只对客户端发送的消息调用):
- 好友关系(应用开启了“只能给好友发消息”)和黑名单:被对方拉黑的用户,服务端仍可以以他的身份给对方发消息;
- 全局禁言、群内禁言和全员禁言;
- 只有群主和管理员可以 @ 全体成员;
- 按用户、按会话、按接收者的发送频率;
- 应用开启
media_url_only后对附件地址的限制(url_not_allowed),见附件的地址。
如果你的业务需要这些限制,请在调用前自行判断。
附件:发送图片、语音、视频和文件消息前,先以用途 attachment 上传文件,把完成上传后返回的文件地址写进 body,见附件的地址。服务端发送时只检查地址的格式,不检查地址是否为本服务的文件地址,也不检查文件是否存在、是否属于本应用、是否已完成上传、是否已过期或被屏蔽:这些情况下消息照常发出,接收方换取下载地址时才会得到 not_found、file_expired 或 file_blocked。群文件的地址也可以发送,但在客户端只有群成员能下载它。
限流:以下限额对客户端和服务端发送的消息合计计算,超出时返回 429 rate_limited,请按 Retry-After 响应头等待后重试:
| 限额 | 数值 | details.reason |
|---|---|---|
| 每个群每秒的消息数 | 40 条 | group_send_rate |
| 应用每分钟的消息数 | 应用的消息配额,见限流与应用状态。批量发送按接收人数计入,只推在线的消息也计入;群提示不计入 | app_message_rate |
此外,发送请求和其他 OpenAPI 请求一样计入应用每秒的调用次数,批量发送按接收人数计入。
{
"error": {
"code": "rate_limited",
"message": "群消息过于频繁,请稍后再试",
"details": { "reason": "group_send_rate" },
"request_id": "X4OO37QZ2ERFSL6DY37HVNA2GV"
}
}发送者的已读位置:服务端以某个用户的身份发送,并不代表他本人看过这个会话。发送者在这个会话中原本没有未读消息时,他的已读位置前进到这条消息;原本有未读的,已读位置不变,这条消息也计入他自己的未读数。两种情况都不会改变对方看到的已读位置和群已读回执。需要时可以代用户标记已读。
发送单条消息
发送一条单聊或群聊消息。to_user 和 to_group 必须恰好给出一个:给 to_user 时发送单聊消息,必须给出 from;给 to_group 时发送群消息,省略 from 时以系统身份发送(sender 为 null,sender_type 为 system)。
/{org_name}/{app_name}/messages路径参数
org_name、app_name 见接入概述。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
from | String | 单聊必填 | 发送者的用户名。发送群消息时可以省略,省略时以系统身份发送 |
to_user | String | 二选一 | 单聊的接收者用户名,不能是 from 本人 |
to_group | String | 二选一 | 群 ID,即群聊的会话 ID |
client_msg_id | String | 否 | 去重 ID,1 到 64 个可见 ASCII 字符(不含空格),不能以 evt_、sys_ 开头。同一发送者在同一会话中相同的 client_msg_id 只写入一次。不传时由服务端生成(在内容安全检查和发消息前回调之前生成,回调收到的与最终写入的相同),这时重试会产生重复的消息 |
type | String | 是 | 消息类型:text、image、voice、video、file、location、custom,见消息类型 |
body | Object | 是 | 消息体,格式由 type 决定 |
ext | Object | 否 | 扩展字段 |
mentions | Object | 否 | @ 提及:usernames(最多 20 个)和 all。只能用于群消息 |
reply_to | Object | 否 | 引用回复:{ "seq": 序号 },引用同一会话中的一条消息 |
need_receipt | Boolean | 否 | 是否要求群已读回执,默认 false。只能用于指定了发送者的群消息,且应用需要在运行策略中开启群已读回执 |
exclude_from_unread | Boolean | 否 | 是否不计入接收者的未读数,默认 false。用于不需要提醒的通知类消息;这样的消息也不会让接收者已删除的会话重新出现在会话列表中 |
push | Object | 否 | 这条消息的离线推送选项:disabled(不推送)、force(不受接收者的免打扰限制,不能与 disabled 同时为 true)、title(通知的标题,1 到 64 个字符)、body(通知的正文,1 到 256 个字符)、ext(交给 App 的自定义数据,JSON 对象,不超过 512 字节),不能有其他字段;整个对象按服务端的编码不超过 2 KB(2048 字节)。见发送消息时的 push 字段 |
online_only | Boolean | 否 | 是否只推在线,默认 false。为 true 时只能是 custom 类型,不能同时带 mentions、reply_to、need_receipt、exclude_from_unread 和 push |
sync_to_sender | Boolean | 否 | 是否同步给发送者的设备,默认 true。只对单聊消息和只推在线的消息有效;普通群消息总是同步给发送者 |
sync_to_sender 为 false 时,消息不推送到发送者的设备,也不会立即出现在发送者的会话列表中(直到这个会话下一次有其他变化),但他拉取这个会话的历史消息时仍能看到它。用一个系统账号给大量用户发通知时,建议设为 false:每条同步给发送者的消息都要更新这个账号的会话列表,一个账号短时间内给成千上万人发消息会明显变慢。
请求示例
以 alice 的身份给 bob 发送一条单聊文本消息:
curl -X POST "$IM_API/$ORG/$APP/messages" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": "alice",
"to_user": "bob",
"client_msg_id": "doc-msg-0001",
"type": "text",
"body": { "text": "你好,明天上午十点评审" }
}'用通知账号给用户发一张订单卡片,不同步给通知账号的设备:
curl -X POST "$IM_API/$ORG/$APP/messages" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": "notice",
"to_user": "carol",
"client_msg_id": "order-20261003-8812",
"type": "custom",
"body": { "card": "order_shipped", "order_id": "8812", "title": "你的订单已发货" },
"sync_to_sender": false
}'以 alice 的身份给 bob 发送一张已上传的图片,url、thumbnail_url、width、height、size 取自完成上传后返回的文件对象:
curl -X POST "$IM_API/$ORG/$APP/messages" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": "alice",
"to_user": "bob",
"client_msg_id": "doc-img-0001",
"type": "image",
"body": {
"url": "https://im.example.com/media/v1/f/99606017553203200/5nOzK6E40hgYIKl80dYVLQ",
"thumbnail_url": "https://im.example.com/media/v1/f/99606017553203200/5nOzK6E40hgYIKl80dYVLQ/thumb",
"width": 1080,
"height": 720,
"size": 218935,
"format": "jpg"
}
}'以系统身份在群里发送一条 @ 全体成员的通知:
curl -X POST "$IM_API/$ORG/$APP/messages" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_group": "99582580415791104",
"client_msg_id": "maintain-20261003",
"type": "text",
"body": { "text": "本群将于今晚 22:00 维护" },
"mentions": { "all": true }
}'响应
成功返回 201 Created,响应体为写入的消息对象。以第一个示例为例:
{
"conversation_id": "99582604130385920",
"conversation_type": "single",
"seq": 1,
"message_id": "99582604155551744",
"client_msg_id": "doc-msg-0001",
"sender": "alice",
"sender_type": "user",
"recipient": "bob",
"type": "text",
"body": { "text": "你好,明天上午十点评审" },
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:05:42.986Z",
"message_version": 0
}两人之间的第一条消息会创建单聊会话,响应中的 conversation_id 就是这个会话的 ID。
内容被替换:消息中命中内容安全替换规则的文字被替换为 *,发消息前回调也可能改写 body 和 ext,保存和推送给接收者的都是替换、改写后的内容。请以响应中的 body、ext 为准。
重复提交:用相同的 client_msg_id 重试时返回 200 OK 和第一次写入的消息,即使这次请求的内容与第一次不同。重复提交不再做权限检查和限流,不消耗发送额度。
只推在线的消息:online_only 为 true 时返回 200 OK 和精简的结果,消息不保存,没有会话 ID 和序号:
| 字段 | 类型 | 说明 |
|---|---|---|
message_id | String | 这条消息的 ID,只用于标识,不能用来查询 |
client_msg_id | String | 请求中的 client_msg_id;没有给出时与 message_id 相同 |
created_at | String | 发送时间 |
online_only | Boolean | 固定为 true |
{
"message_id": "99582734451605504",
"client_msg_id": "sig-001",
"created_at": "2026-10-02T19:06:14.051Z",
"online_only": true
}错误
| HTTP 状态码 | code | 说明 |
|---|---|---|
| 400 | invalid_argument | details.reason 为 invalid_recipient:to_user 和 to_group 都给了或都没给 |
| 400 | invalid_argument | details.reason 为 from_required:发送单聊消息没有给出 from |
| 400 | invalid_argument | details.reason 为 self_conversation:to_user 是发送者本人 |
| 400 | invalid_argument | details.reason 为 unknown_type:不支持的消息类型,包括只由系统写入的 tip 和 call |
| 400 | invalid_argument | details.reason 为 invalid_body:消息内容(包括发消息前回调改写后的内容)不符合要求,或 mentions.usernames 超过 20 个,details.field 指出字段 |
| 400 | invalid_argument | details.reason 为 invalid_client_msg_id:client_msg_id 的格式不对 |
| 400 | invalid_argument | details.reason 为 reserved_client_msg_id:client_msg_id 以 evt_ 或 sys_ 开头 |
| 400 | invalid_argument | details.reason 为 mentions_not_allowed:单聊消息带了 mentions |
| 400 | invalid_argument | details.reason 为 receipt_not_allowed:单聊消息或以系统身份发送的群消息要求了已读回执 |
| 400 | invalid_argument | details.reason 为 invalid_reply:引用的消息不存在、已撤回、已过期,或发送者看不到 |
| 400 | invalid_argument | details.reason 为 invalid_online_only:只推在线的消息不是 custom 类型,或带了不允许的字段 |
| 400 | invalid_argument | details.reason 为 invalid_push:push 不是 JSON 对象、超过 2 KB,或其中的字段不符合要求,details.field 指出字段(如 title、force) |
| 403 | permission_denied | 要求了群已读回执,但应用没有开启群已读回执,details.reason 为 read_ack_disabled |
| 403 | not_group_member | 发送者不是群成员 |
| 403 | user_disabled | 发送者已被平台封禁,details.disabled_by 为 platform |
| 403 | group_disabled | 群已被平台封禁,details.disabled_by 为 platform |
| 403 | message_rejected | 消息未通过内容安全检查,details.reason 为 sensitive_content、provider_block 或 check_timeout,见调用方看到的错误;或被发消息前回调拒绝,details.reason 为 app_rejected(你的回调接收端拒绝)或 callback_unavailable(回调失败后按“拒绝”策略处理),details.app_reason 为接收端给出的原因代码,message 为接收端给出的提示,见操作方收到的错误 |
| 403 | app_unavailable | 应用处于只读状态,不能发送消息,见限流与应用状态 |
| 404 | not_found | 发送者或接收者不存在或已被删除(“用户不存在”);群不存在或已解散(“会话不存在”) |
| 413 | payload_too_large | 消息(包括发消息前回调改写后的消息)超过应用的大小上限,details.max_bytes 为上限,见大小上限 |
| 429 | rate_limited | 超过发送限流,details.reason 为 group_send_rate 或 app_message_rate |
批量发送消息
以同一个发送者、同样的内容,给一批用户各发一条单聊消息,用于系统通知、营销消息等。每个接收人分别处理,某个人失败不影响其他人。
/{org_name}/{app_name}/messages/batch- 一次最多给应用运行策略中
max_recipients_per_message个用户发送,默认 600,可设 1 到 1000; - 必须带
client_msg_id:它在每个接收人的会话中各用一次。超时后用同样的参数原样重试,已经发出的不会重复发送,结果为duplicate; - 默认不同步给发送者的设备(
sync_to_sender默认false,与单条发送相反); - 不能 @、不能引用消息、不能要求已读回执,也不能只推在线;
- 调用一次按接收人数计入应用每秒的调用次数和每分钟的消息数。应用每分钟的消息数不够时,超出的接收人返回
rate_limited,其余照常发送。
路径参数
org_name、app_name 见接入概述。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
from | String | 是 | 发送者的用户名 |
to_users | Array<String> | 是 | 接收者的用户名,1 到 max_recipients_per_message 个。重复的用户名只发送一次,各自返回相同的结果 |
client_msg_id | String | 是 | 去重 ID,规则同发送单条消息 |
type | String | 是 | 消息类型 |
body | Object | 是 | 消息体 |
ext | Object | 否 | 扩展字段 |
exclude_from_unread | Boolean | 否 | 是否不计入接收者的未读数,默认 false |
push | Object | 否 | 离线推送的选项,规则同发送单条消息 |
sync_to_sender | Boolean | 否 | 是否同步给发送者的设备,默认 false |
请求示例
curl -X POST "$IM_API/$ORG/$APP/messages/batch" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": "notice",
"to_users": ["alice", "bob", "nobody", "notice"],
"client_msg_id": "notice-20261003-001",
"type": "text",
"body": { "text": "系统将于今晚 22:00 升级" }
}'响应
成功返回 200 OK,results 中按 to_users 的顺序列出每个接收人的结果:
| 字段 | 类型 | 说明 |
|---|---|---|
username | String | 接收者的用户名,与请求中相同 |
status | String | 成功时为 sent(这次发送)或 duplicate(之前已经发送过,返回第一次的结果) |
conversation_id | String | 成功时为单聊会话的 ID |
seq | Number | 成功时为消息在会话中的序号 |
message_id | String | 成功时为消息 ID |
code | String | 失败时为错误码,如 not_found、rate_limited |
message | String | 失败时为错误说明 |
details | Object | 失败时的详细信息,可为 null |
{
"results": [
{ "username": "alice", "status": "sent", "conversation_id": "99583086802501633", "seq": 1, "message_id": "99583086898970624" },
{ "username": "bob", "status": "sent", "conversation_id": "99583086802501632", "seq": 1, "message_id": "99583086827667456" },
{ "username": "nobody", "code": "not_found", "message": "用户不存在", "details": null },
{ "username": "notice", "code": "invalid_argument", "message": "不能给自己发消息", "details": { "reason": "self_conversation" } }
]
}用同样的参数再次调用时,已经发送过的接收人的 status 为 duplicate,seq、message_id 与第一次相同。
错误
以下错误针对整个请求,这时没有给任何人发送。单个接收人的失败在 results 中返回,可能的 code 有 not_found(接收者不存在或已删除)、invalid_argument(self_conversation)、rate_limited(app_message_rate)、app_unavailable(应用处于只读状态)、message_rejected(内容未通过内容安全检查,或被发消息前回调拒绝:两者都只调用一次,结果对全部接收人生效,被拒绝时每个接收人都返回这个错误)。
| HTTP 状态码 | code | 说明 |
|---|---|---|
| 400 | invalid_argument | details.reason 为 from_required:没有给出 from |
| 400 | invalid_argument | details.reason 为 too_many_items:to_users 为空或超过上限,details.max 为上限 |
| 400 | invalid_argument | details.reason 为 invalid_client_msg_id:没有给出 client_msg_id 或格式不对;以 evt_、sys_ 开头时为 reserved_client_msg_id |
| 400 | invalid_argument | details.reason 为 unknown_type、invalid_body、invalid_push:内容不符合要求,同发送单条消息 |
| 400 | invalid_argument | details.reason 为 invalid_reply:批量发送带了 reply_to |
| 403 | user_disabled | 发送者已被平台封禁 |
| 404 | not_found | 发送者不存在或已被删除 |
| 413 | payload_too_large | 消息超过应用的大小上限 |
