傳送訊息
從你的業務服務端發訊息:以某個使用者的身份發單聊或群聊、以系統身份發群通知,或者給一批使用者各發一條。常見用途是系統通知、訂單狀態、客服訊息。
快速開始
以 alice 的身份給 bob 發一條文字訊息(IM_API、ORG、APP、APP_TOKEN 的設定見接入概述):
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": "你好,明天上午十点评审" }
}'返回 201 Created 和寫入的訊息。bob 線上的裝置立刻收到;不線上的,下次登入時同步到,App 在後臺時還會收到離線推送。完整的響應見響應。
按發給誰選擇欄位:
| 發給 | 欄位 | 傳送者 |
|---|---|---|
| 一個使用者(單聊) | from + to_user | from |
| 一個群 | from + to_group | from,必須是群成員 |
| 一個群,以系統身份 | 只給 to_group | 系統,群裡顯示為系統通知 |
| 一批使用者,每人一條單聊 | from + to_users,用批次傳送 | from |
每條訊息都帶上 client_msg_id
超時後你不知道上次是否發成功了。給每條訊息一個唯一的 client_msg_id(業務單號或 UUID),重試時原樣帶上,服務端只寫入一次,不會出現重複訊息。
客戶端可以用訊息的 via 欄位區分來源:服務端發的訊息 via 為 openapi。
常見場景
用通知賬號發業務卡片
custom 訊息的 body 由你定義,客戶端按自己的約定顯示成卡片。通知賬號給大量使用者發訊息時,把 sync_to_sender 設為 false,原因見請求體下方的說明:
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
}'使用者在客戶端也能發出看起來一樣的 custom 訊息,所以涉及訂單、資金的卡片,客戶端只應信任服務端發出的(Web SDK 中 _local.trusted 為 true)。
傳送圖片
分兩步:先以用途 attachment 上傳檔案,再把返回的檔案物件中的 url、thumbnail_url、width、height、size 填進 body:
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"
}
}'語音、影片、檔案訊息的做法相同,body 的欄位見訊息格式。服務端不檢查檔案是否真的存在:地址寫錯時訊息照樣發出,接收方下載時才報錯,見附件。
以系統身份發群通知
不給 from,訊息以系統身份發到群裡。下面的通知還 @ 了全體成員:
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 }
}'其他場景
| 需求 | 做法 |
|---|---|
| 給幾百個使用者各發一條通知 | 批次傳送訊息 |
| 通知不計入未讀、不提醒 | exclude_from_unread: true |
| 自定義推送的標題和正文,或不推送 | push 欄位,見傳送訊息時的 push 欄位 |
| 引用回覆某條訊息 | reply_to: { "seq": 序号 } |
| 要求群成員回執“已讀” | need_receipt: true,需要在執行策略中開啟群已讀回執 |
| 只推給線上裝置、不儲存的信令 | online_only: true,見只推線上的訊息 |
| 撤回、編輯、置頂已發出的訊息 | 撤回、編輯與置頂 |
| 代使用者標記已讀 | 標記已讀 |
傳送單條訊息
/{org_name}/{app_name}/messagesto_user 和 to_group 二選一。單聊必須給出 from;群訊息省略 from 時以系統身份傳送(sender 為 null,sender_type 為 system)。
路徑引數
org_name、app_name 見接入概述。
請求體
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
from | String | 單聊必填 | 傳送者的使用者名稱。群訊息省略時以系統身份傳送 |
to_user | String | 二選一 | 單聊的接收者,不能是 from 本人 |
to_group | String | 二選一 | 群 ID(即群聊的會話 ID) |
type | String | 是 | text、image、voice、video、file、location、custom,見訊息型別 |
body | Object | 是 | 訊息內容,格式由 type 決定 |
client_msg_id | String | 建議 | 去重 ID,1 到 64 個可見 ASCII 字元,不含空格,不能以 evt_、sys_ 開頭。不傳時由服務端生成,但這樣重試會產生重複訊息 |
ext | Object | 否 | 你的擴充套件欄位,原樣儲存,見擴充套件欄位 |
mentions | Object | 否 | 群訊息的 @ 提及:usernames(最多 20 個)或 all |
reply_to | Object | 否 | 引用回覆:{ "seq": 序号 },同一會話中的訊息 |
need_receipt | Boolean | 否 | 要求群已讀回執,預設 false。只用於有傳送者的群訊息 |
exclude_from_unread | Boolean | 否 | 不計入接收者的未讀數,預設 false。這樣的訊息也不會讓接收者已刪除的會話重新出現 |
push | Object | 否 | 這條訊息的離線推送,見下表 |
online_only | Boolean | 否 | 只推線上,預設 false。為 true 時只能是 custom,且不能帶 mentions、reply_to、need_receipt、exclude_from_unread、push |
sync_to_sender | Boolean | 否 | 同步給傳送者的裝置,預設 true。只對單聊和只推線上的訊息有效,見下文 |
push 的欄位(整個物件不超過 2 KB):
| 欄位 | 說明 |
|---|---|
disabled | true 時不推送 |
force | true 時不受接收者的免打擾限制;不能與 disabled 同時為 true |
title | 通知標題,1 到 64 個字元 |
body | 通知正文,1 到 256 個字元 |
ext | 交給 App 的自定義資料,JSON 物件,不超過 512 位元組 |
什麼時候把 sync_to_sender 設為 false:用一個系統賬號給大量使用者發通知時。設為 false 後,訊息不推送到傳送者的裝置,也不會立即出現在他的會話列表中,但拉取歷史時仍能看到。否則每條訊息都要更新這個賬號的會話列表,短時間給成千上萬人傳送會明顯變慢。
響應
成功返回 201 Created 和寫入的訊息物件。通常只需要儲存 conversation_id、seq 和 message_id:
{
"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 |
用相同的 client_msg_id 重試 | 200 OK 和第一次寫入的訊息,即使這次內容不同;不再檢查許可權和限流,不佔傳送額度 |
| 內容被內容安全替換,或被發訊息前回撥改寫 | 儲存和推送的是改寫後的內容,請以響應中的 body、ext 為準 |
online_only 為 true | 200 OK 和下面的精簡結果;訊息不儲存,沒有會話 ID 和序號 |
只推線上的訊息返回:
| 欄位 | 型別 | 說明 |
|---|---|---|
message_id | String | 訊息的 ID,只用於標識,不能用來查詢 |
client_msg_id | String | 請求中的值;沒有給出時與 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
}錯誤
請求引數不對時返回 400 invalid_argument,用 details.reason 區分:
details.reason | 原因 |
|---|---|
invalid_recipient | to_user 和 to_group 都給了,或都沒給 |
from_required | 單聊沒有給出 from |
self_conversation | to_user 是傳送者本人 |
unknown_type | 不支援的訊息型別,包括只能由系統寫入的 tip、call |
invalid_body | 訊息內容(包括回撥改寫後的)不符合要求,或 mentions.usernames 超過 20 個;details.field 指出欄位 |
invalid_client_msg_id | client_msg_id 格式不對 |
reserved_client_msg_id | client_msg_id 以 evt_ 或 sys_ 開頭 |
mentions_not_allowed | 單聊訊息帶了 mentions |
receipt_not_allowed | 單聊或系統身份的群訊息要求了已讀回執 |
invalid_reply | 引用的訊息不存在、已撤回、已過期,或傳送者看不到 |
invalid_online_only | 只推線上的訊息不是 custom,或帶了不允許的欄位 |
invalid_push | push 不是物件、超過 2 KB 或欄位不對;details.field 指出欄位 |
其他錯誤:
| HTTP 狀態碼 | code | 原因 |
|---|---|---|
| 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 | 內容沒透過稽核,或被髮訊息前回撥拒絕,見下表 |
| 403 | app_unavailable | 應用處於只讀狀態,見限流與應用狀態 |
| 404 | not_found | 傳送者或接收者不存在、已刪除;群不存在或已解散 |
| 413 | payload_too_large | 訊息(包括回撥改寫後的)超過應用的大小上限,details.max_bytes 為上限,見大小上限 |
| 429 | rate_limited | 超過傳送限流,按 Retry-After 等待後重試,見限流 |
message_rejected 的 details.reason:
details.reason | 原因 |
|---|---|
sensitive_content、provider_block | 內容安全攔截,見呼叫方看到的錯誤 |
app_rejected | 你的發訊息前回撥拒絕;details.app_reason 是你給出的原因程式碼,message 是你給出的提示 |
callback_unavailable | 回撥失敗,按“拒絕”策略處理,見操作方收到的錯誤 |
批次傳送訊息
同一個傳送者、同樣的內容,給一批使用者各發一條單聊訊息。每個人單獨處理,某人失敗不影響其他人。
/{org_name}/{app_name}/messages/batch和傳送單條訊息的不同:
| 單條傳送 | 批次傳送 | |
|---|---|---|
| 接收者 | 一個使用者或一個群 | 最多 max_recipients_per_message 個使用者(預設 600,可設 1 到 1000) |
client_msg_id | 建議帶 | 必須帶,在每個接收人的會話中各用一次 |
sync_to_sender 預設值 | true | false |
| @、引用、已讀回執、只推線上 | 支援 | 不支援 |
| 限流 | 按 1 條計 | 按接收人數計入每秒呼叫次數和每分鐘訊息數;額度不夠時超出的人返回 rate_limited,其餘照常傳送 |
超時後用同樣的引數原樣重試即可:已經發出的不會重複發,結果為 duplicate。
路徑引數
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 的順序列出每個人的結果。下面的例子中 alice、bob 成功,nobody 不存在,notice 是傳送者本人:
{
"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" } }
]
}| 欄位 | 型別 | 說明 |
|---|---|---|
username | String | 接收者,與請求中相同 |
status | String | 成功時為 sent(這次發出)或 duplicate(之前發過,返回第一次的結果) |
conversation_id、seq、message_id | String、Number、String | 成功時的會話 ID、序號和訊息 ID |
code、message、details | String、String、Object | 失敗時的錯誤碼、說明和詳細資訊(可為 null) |
單個接收人可能的失敗:
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(沒給或格式不對)或 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 | 訊息超過應用的大小上限 |
服務端傳送的規則
服務端傳送時,誰能給誰發由你的業務決定。服務端只做少量檢查,客戶端傳送時的很多限制在這裡不生效。
會檢查的
| 檢查 | 不透過時 |
|---|---|
| 傳送者存在且未刪除 | 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),見附件的地址。
附件
服務端只檢查附件地址的格式。以下情況訊息照樣發出,接收方換取下載地址時才報錯(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"
}
}傳送者的未讀數
以某個使用者的身份傳送,不代表他看過這個會話:
| 傳送前,傳送者在這個會話中 | 傳送後 |
|---|---|
| 沒有未讀訊息 | 已讀位置移到這條訊息,未讀數仍為 0 |
| 有未讀訊息 | 已讀位置不變,這條訊息也計入他自己的未讀數 |
兩種情況都不影響對方看到的已讀狀態和群已讀回執。需要時可以代使用者標記已讀。
