消息格式
本页说明会话和消息的基本概念、各消息类型的内容格式和消息对象的字段。发送消息、查询与导出消息、撤回、编辑与置顶以及会话的各个接口都使用这里的定义。
消息有三种来源:用户在客户端发送;你的业务服务端通过 OpenAPI 以某个用户的身份发送,或以系统身份发送群消息;服务在群成员和资料变化时自动写入群提示,在音视频通话结束后自动写入通话记录。三者的格式相同,用消息对象的 via 字段区分。
会话 ID
会话是两个用户之间(单聊)或一个群中(群聊)的消息流,用 conversation_id 标识,conversation_type 为 single 或 group。
- 单聊:两个用户之间只有一个单聊会话。会话在两人之间的第一条消息写入时创建,ID 由服务端生成,不能由用户名推算。获得单聊会话 ID 的方式:
- 群聊:会话 ID 就是群 ID(
group_id),建群之后即可使用,不需要另外获取。
用户被删除后,即使同名的用户名被重新注册,新用户与对方之间也是另一个会话,看不到原来的消息。
序号 seq
每条消息在会话中有一个序号 seq,从 1 开始连续递增,单聊双方看到的序号相同。群提示也占用序号;只推在线的消息不保存,没有序号。
- 序号只在会话内唯一。要在全应用范围内标识一条消息,使用
message_id。 - 客户端按序号判断是否漏收了消息,历史消息也按序号翻页,见会话与历史消息。
消息 ID
message_id 是消息在全应用范围内唯一的 ID(字符串),大体随发送时间递增。服务端的按 ID 查询、撤回、编辑、置顶等接口都用它指定消息,导出也按它的顺序进行。
消息类型
type | 说明 | 谁可以发送 |
|---|---|---|
text | 文本 | 客户端、服务端 |
image | 图片 | 客户端、服务端 |
voice | 语音 | 客户端、服务端 |
video | 视频 | 客户端、服务端 |
file | 文件 | 客户端、服务端 |
location | 位置 | 客户端、服务端 |
custom | 自定义消息,结构由你定义,如订单卡片、红包、业务通知 | 客户端、服务端 |
tip | 群提示,由服务在群变化时自动写入,见群提示 | 只有系统 |
call | 通话记录,由服务在音视频通话结束后自动写入,见通话记录 | 只有系统 |
发送 tip、call 或其他不支持的类型返回 400 invalid_argument,details.reason 为 unknown_type。消息类型之后可能增加,客户端遇到不认识的类型时应显示为“当前版本不支持此消息”,不要丢弃,否则会话中的序号会不连续。
各类型的消息体 body 都是 JSON 对象,字段如下。表中没有列出的字段不做检查,会原样保存和返回,可以用来携带你自己的数据。图片、语音、视频和文件消息中的文件要先上传,再把得到的地址写进 body,见附件的地址。
文本 text
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
text | String | 是 | 文本内容,不能为空或只有空白字符。可以包含换行、回车和制表符 |
{ "text": "你好,明天上午十点评审" }消息正文中的“@张三”文字由发送方自己拼写,服务端只根据 mentions 判断谁被 @ 了。
图片 image
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | String | 是 | 图片地址:上传后得到的文件地址,即文件对象的 url |
thumbnail_url | String | 否 | 缩略图地址:文件对象的 thumbnail_url(长边不超过 480 像素) |
width | Number | 否 | 宽度,像素,非负整数。取文件对象的 width |
height | Number | 否 | 高度,像素,非负整数。取文件对象的 height |
size | Number | 否 | 文件大小,字节,非负整数。取文件对象的 size |
format | String | 否 | 格式,如 jpg、png,可以按文件对象的 content_type 填写 |
{
"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"
}宽高和大小请使用文件对象中的值:服务端会去掉图片中的拍摄信息等元数据,保存后的大小可能与原文件不同。
语音 voice
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | String | 是 | 语音的文件地址 |
duration_seconds | Number | 是 | 时长,秒,0 到 86400 之间的整数。服务端不解析音频,由发送方填写 |
size | Number | 否 | 文件大小,字节,非负整数 |
format | String | 否 | 格式,如 amr、aac |
{
"url": "https://im.example.com/media/v1/f/99607894634266624/BW6oGMSauXheems_KKZ5HQ",
"duration_seconds": 3,
"size": 48044,
"format": "wav"
}视频 video
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | String | 是 | 视频的文件地址 |
duration_seconds | Number | 是 | 时长,秒,0 到 86400 之间的整数。服务端不解析视频,由发送方填写 |
thumbnail_url | String | 否 | 封面图地址。服务端不截取视频的封面:请在客户端截取一帧,作为图片另外上传,填写它的文件地址或缩略图地址 |
width | Number | 否 | 宽度,像素,非负整数 |
height | Number | 否 | 高度,像素,非负整数 |
size | Number | 否 | 文件大小,字节,非负整数 |
format | String | 否 | 格式,如 mp4 |
{
"url": "https://im.example.com/media/v1/f/99607911269875712/q0Y8RkM2c1ZbJx4TnW6aPg",
"duration_seconds": 30,
"thumbnail_url": "https://im.example.com/media/v1/f/99607911324401664/Hk7dVw2LmQ9sTzR5yB1cNe/thumb",
"width": 1280,
"height": 720,
"size": 3048000,
"format": "mp4"
}文件 file
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | String | 是 | 文件地址 |
name | String | 是 | 文件名,1 到 255 个字符,不能只有空白,不能包含 /、\、控制字符和改变文字方向的字符(U+202A 到 U+202E、U+2066 到 U+2069)。规则与上传时的 name 相同,通常填上传时的文件名 |
size | Number | 否 | 文件大小,字节,非负整数 |
format | String | 否 | 格式,如 pdf |
{
"url": "https://im.example.com/media/v1/f/99606363390345216/qKZTLgqgze82AN96YOCJzg",
"name": "季度报告.pdf",
"size": 1048576,
"format": "pdf"
}位置 location
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
latitude | Number | 是 | 纬度,-90 到 90 |
longitude | Number | 是 | 经度,-180 到 180 |
name | String | 否 | 地点名称 |
address | String | 否 | 详细地址 |
{ "latitude": 39.9087, "longitude": 116.3975, "name": "天安门", "address": "北京市东城区" }自定义 custom
body 可以是任意 JSON 对象,结构由你定义,只受下文通用规则的限制。
{ "card": "order_shipped", "order_id": "8812", "title": "你的订单已发货" }业务卡片的可信度
custom 的内容由发送方决定,用户在客户端也能发出看起来像“转账”“订单”的卡片。涉及资金、订单状态的卡片,客户端只应信任 via 不是 client 的消息,即由你的服务端或系统写入的消息。
通话记录 call
音视频通话结束后,服务在会话中写入一条通话记录,via 为 system,client_msg_id 以 sys_ 开头。一对一通话写入两人的单聊会话,发送者为主叫;群通话写入群会话,发送者为发起人,他已不在群里时以系统身份写入(sender 为 null,sender_type 为 system)。客户端和你的服务端都不能发送这种类型的消息。哪些通话写入、是否计入未读数、如何关闭,见通话记录。
| 字段 | 类型 | 说明 |
|---|---|---|
call_id | String | 通话 ID,可以用它查询通话详情 |
call_type | String | single 一对一 / group 群通话 |
media | String | audio 语音 / video 视频 |
result | String | 通话的结束原因,取值见结束原因 |
duration_seconds | Number | 通话时长,秒,没有接通为 0 |
started_at | String | 发起通话的时间 |
{
"call_id": "100313666666102784",
"call_type": "group",
"media": "video",
"result": "completed",
"duration_seconds": 32,
"started_at": "2026-10-04T19:30:41.871Z"
}内容的通用规则
- 地址:
url、thumbnail_url必须是带主机名的http或https地址,最长 2048 个字符。服务端不访问地址,也不检查它指向的文件是否存在;应用开启media_url_only后,客户端发送的地址另有限制,见附件的地址。 - JSON 对象:
body和ext都必须是 JSON 对象,嵌套不超过 10 层,键的总数不超过 256 个,不能有重复的键。 - 控制字符:所有字符串(包括键)都不能包含控制字符,只有文本消息的
text允许换行、回车和制表符。 - 数字:宽高、大小、时长必须是非负整数,不能带小数。
不符合时返回 400 invalid_argument,details.reason 为 invalid_body,details.field 指出出错的字段:
{
"error": {
"code": "invalid_argument",
"message": "url 必须是 http 或 https 地址,最长 2048 个字符",
"details": { "field": "url", "reason": "invalid_body" },
"request_id": "JB2OCKPROHY2SZ2Q4VNURTDZWW"
}
}附件的地址
图片、语音、视频和文件要先以用途 attachment 上传到本服务(由客户端上传,或由你的服务端通过上传文件上传),完成上传后得到文件地址,形如 https://im.example.com/media/v1/f/{file_id}/{secret};图片另有缩略图地址,即文件地址末尾加 /thumb。把它们写进 body 的 url、thumbnail_url 后再发送消息。
- 发送时不检查文件:服务端只检查地址的格式,不检查文件是否存在、是否已完成上传、是否已过期或被屏蔽。请使用完成上传后返回的地址;地址写错时消息照常发出,接收方下载时才会失败。
- 下载:文件地址本身不能直接下载。客户端用 User Token 换取短期有效的下载地址,或带着 User Token 直接请求文件地址(返回
302,跳转到下载地址);你的服务端通过 OpenAPI 换取,见下载与管理文件。本应用登录的用户持有地址就能换取,不按会话判断;只有群文件的地址要求客户端的用户是群成员。 - 过期与删除:附件从上传时起保留运行策略中
attachment_retention_days天(默认 30 天),到期后文件被删除,消息仍然保留,body中的地址也不变。之后换取下载地址时得到file_expired(直接请求文件地址返回410),客户端应显示“文件已过期”。你的服务端删除文件后同样如此;撤回消息不会删除附件。删除约 30 天后再换取返回not_found,客户端按同样的方式显示即可。 - 屏蔽:文件因违规被屏蔽后,换取下载地址时得到
file_blocked(直接请求文件地址返回403),客户端应显示文件已被屏蔽。消息本身不变,需要时另行撤回。 - 外部地址:应用没有开启
media_url_only(默认)时,url、thumbnail_url也可以是任意http、https地址,服务端原样保存,不会访问。这样的文件不经过本服务,不受上述保留期和屏蔽的约束。
地址限制:应用在运行策略中开启 media_url_only 后,用户在客户端发送图片、语音、视频和文件消息时,url、thumbnail_url 只能是本服务的文件地址或缩略图地址,或者 media_allowed_hosts 中主机的 https 地址(*.example.com 匹配它的任意一级子域名)。不符合时返回 400 invalid_argument,details.reason 为 url_not_allowed,details.field 指出字段。这项检查只看地址的形式,不查询文件;你的服务端发送的消息不做这项检查。
{
"error": {
"code": "invalid_argument",
"message": "附件地址不被允许",
"details": { "field": "thumbnail_url", "reason": "url_not_allowed" },
"request_id": "AK2OWVVKVS4JWVKUNOGDRTGPMR"
}
}扩展字段 ext
每种类型的消息都可以带 ext:你自定义的扩展内容(JSON 对象),如业务 ID、消息来源、转发来源。规则与 body 相同,服务端原样保存和返回。不需要时省略或传 null。
@ 提及
群消息可以用 mentions @ 成员或全体成员,单聊不能使用(返回 invalid_argument,mentions_not_allowed)。
| 字段 | 类型 | 说明 |
|---|---|---|
usernames | Array<String> | 被 @ 的成员的用户名,最多 20 个 |
all | Boolean | 是否 @ 全体成员,默认 false |
- 不是群成员的用户(包括不存在的用户名)会被忽略,不返回错误;响应中的
mentions.usernames只包含实际被 @ 的成员。 - 被 @ 的成员的会话会显示“有人 @ 我”,直到他读到这条消息,见会话与历史消息。这条消息被撤回后,提醒退回到之前一条仍然有效的 @。
- 客户端只有群主和管理员可以 @ 全体成员;服务端发送不受这一限制。
引用回复
消息可以用 reply_to 引用同一会话中的一条消息,请求中只需给出序号:
"reply_to": { "seq": 3 }被引用的消息必须存在、没有被撤回或擦除、在保留期内;指定了发送者时,还必须是发送者能看到、且没有被他删除的消息,否则返回 invalid_argument,details.reason 为 invalid_reply。
服务端只保存被引用消息的序号和发送者,返回为 { "seq": 3, "sender": "alice" }。客户端按序号从本地或服务端取得原消息显示;原消息之后被撤回时,显示相应的提示,不会泄露撤回的内容。
大小上限
一条消息的类型、body、ext、mentions 和 reply_to 合计按服务端的 JSON 编码(去掉空白、UTF-8 字节,<、>、& 不转义)计算,不能超过应用运行策略中的 max_message_body_bytes:默认 5120 字节(5 KB),可设 1024 到 32768 字节,见运行策略。超出时返回 413 payload_too_large,details.max_bytes 为当前的上限:
{
"error": {
"code": "payload_too_large",
"message": "消息超过大小上限",
"details": { "max_bytes": 5120 },
"request_id": "NVZEB2A22BM63WPDECDBUIZ4P5"
}
}发送时的 push 字段另计,见发送消息。
消息对象
发送、查询、导出、撤回、编辑、置顶等接口返回的消息都是同一个结构。服务端接口以服务端的视角返回:能看到会话中的全部消息,不受任何用户删除消息、清空聊天记录的影响。
| 字段 | 类型 | 说明 |
|---|---|---|
conversation_id | String | 所在会话的 ID,见会话 ID |
conversation_type | String | single 单聊 / group 群聊 |
seq | Number | 在会话中的序号 |
message_id | String | 消息 ID,全应用唯一 |
client_msg_id | String | 发送时的去重 ID。服务端发送时没有给出的,由服务端生成(32 个十六进制字符);群提示以 evt_ 开头,通话记录以 sys_ 开头 |
sender | String | 发送者的用户名。可为 null:以系统身份发送的消息和群提示,或发送者已被删除 |
sender_type | String | user 用户 / system 系统。sender_type 为 user 而 sender 为 null 表示发送者已被删除 |
recipient | String | 单聊的接收者用户名,发送者的其他设备据此知道发给了谁。群聊为 null;接收者已被删除时也为 null |
type | String | 消息类型 |
body | Object | 消息体,格式见各类型。已撤回、已擦除的消息为 null |
ext | Object | 扩展字段,可为 null |
mentions | Object | 提及:usernames(不含已删除的用户)和 all。没有 @ 时为 null |
reply_to | Object | 引用回复:seq 和 sender(被引用消息的发送者,系统发送的或已删除的为 null)。没有引用时为 null |
need_receipt | Boolean | 是否要求群已读回执 |
exclude_from_unread | Boolean | 是否不计入接收者的未读数。群提示总是 true |
reactions | Array<Object> | 表情回应,每种表情一项,没有时为 null。见下文 |
pinned | Object | 置顶信息 { "by": ..., "at": ... }:by 为置顶的用户,由服务端或控制台置顶时为 null;at 为置顶时间。没有置顶时为 null |
edited | Object | 编辑信息 { "at": ..., "count": ... }:最近一次编辑的时间和累计编辑次数。没有编辑过为 null |
recalled | Object | 撤回信息 { "by": ..., "role": ..., "at": ... },没有撤回为 null。见下文 |
erased | Boolean | 是否已被擦除。应用开启了“删除用户时擦除他的消息”时,被删除的用户发过的消息会被擦除,内容清空 |
via | String | 写入方式:client 客户端 / openapi 服务端 / console 控制台 / system 系统(群提示、通话记录) |
created_at | String | 发送时间。同一会话中,序号越大时间越晚(不会更早) |
message_version | Number | 这条消息最近一次变化(撤回、编辑、擦除、表情回应、置顶)时会话的变更版本号,没有变化过为 0。客户端据此增量获取发生变化的消息 |
reactions每一项为{ "key": "thumbs_up", "count": 12, "users": ["lisi", null, "zhaoliu"] }:key是表情的标识,由你的客户端定义;count是回应的人数;users是前 3 个回应的人,已删除的用户为null。表情回应由用户在客户端添加,需要应用在运行策略中开启message_reaction_enabled。recalled.role为撤回者的身份:sender发送者本人 /owner群主 /admin群管理员 /server服务端或控制台 /platform平台。内容安全按你的规则自动撤回、或你的审核人员确认违规后撤回的,为server;按平台规则撤回的,为platform。recalled.by为撤回的用户,server和platform撤回时为null。- 撤回和擦除之后,
body、ext、mentions、reply_to、reactions、pinned都为null,其他字段保留。
一条引用了其他消息、@ 了成员、带扩展字段的群消息:
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 9,
"message_id": "99584445836689408",
"client_msg_id": "5f0c2a7e-91b4-4d2e-a0c3-6b1e8f2d4c19",
"sender": "bob",
"sender_type": "user",
"recipient": null,
"type": "text",
"body": { "text": "@carol 文档已更新,请确认" },
"ext": { "biz_id": "PRD-2041" },
"mentions": { "usernames": ["carol"], "all": false },
"reply_to": { "seq": 3, "sender": "alice" },
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:13:02.076Z",
"message_version": 0
}一条已被服务端撤回的单聊消息:
{
"conversation_id": "99583031936811008",
"conversation_type": "single",
"seq": 3,
"message_id": "99583385822822400",
"client_msg_id": "d83eb893f17d57d202db27cb79f46ef0",
"sender": "carol",
"sender_type": "user",
"recipient": "dave",
"type": "text",
"body": null,
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": false,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": { "by": null, "role": "server", "at": "2026-10-02T19:08:59.798Z" },
"erased": false,
"via": "openapi",
"created_at": "2026-10-02T19:08:49.350Z",
"message_version": 22
}系统消息
sender_type 为 system、sender 为 null 的消息是系统消息,有两种:
- 以系统身份发送的群消息:服务端发送群消息时省略
from,消息显示为群里的系统通知,via为openapi。 - 群提示:见下文,
type为tip,via为system。
群通话的发起人已不在群里时,通话记录也以系统身份写入。
群提示
群创建、成员加入或离开、资料变化、解散时,服务自动在群会话中写入一条群提示(type 为 tip)。离线的成员上线后在聊天记录中也能看到;被移出的成员能看到移出他的那条提示,新成员能看到自己入群的那条。
- 群提示不计入未读数(
exclude_from_unread总是true),不占用群的发送限流; - 你不能发送群提示(返回
unknown_type),但服务端可以撤回它; - 提示通常比群的变化晚几百毫秒写入,极少数情况下两条提示的先后与实际相反,客户端可以按
body.occurred_at排序显示; - 提示的
body是结构化的,显示的文字由客户端按kind生成,便于多语言。
body 中都有 kind(提示的种类)和 occurred_at(变化发生的时间),其他字段如下。用户一律以用户名表示,已删除的用户为 null:
kind | 含义 | 其他字段 |
|---|---|---|
group_created | 创建了群聊 | operator:群主 |
members_joined | 成员加入 | operator:邀请人,没有邀请人的(服务端建群的初始成员、申请加入、服务端添加)为 null;members:加入的成员,最多列出前 20 个;joined_count:加入的人数;joined_via:加入方式,create 建群 / invite 邀请 / apply 申请 / link 入群链接或二维码 / server 服务端添加 |
members_left | 成员退出,或成员的账号被删除 | operator:为 null;members:离开的成员,最多前 20 个;removed_count:离开的人数 |
members_removed | 成员被移出(包括被加入群黑名单) | operator:操作人,由服务端操作时为 null;members、removed_count 同上 |
group_renamed | 修改了群名称 | operator;name:新的群名称 |
announcement_updated | 更新了群公告 | operator |
owner_changed | 转让了群主 | operator;owner:新群主 |
mute_all_changed | 开启或关闭全员禁言 | operator;enabled:是否开启 |
group_status_changed | 群被封禁或解封 | operator;status:disabled 已封禁 / active 已解封 |
group_dismissed | 群已解散 | dismissed_by:owner 群主 / tenant 你的服务端或控制台 / platform 平台 / system 系统 |
由服务端建群后的两条群提示:
[
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 1,
"message_id": "99582580659060736",
"client_msg_id": "evt_group_created",
"sender": null,
"sender_type": "system",
"recipient": null,
"type": "tip",
"body": { "kind": "group_created", "occurred_at": "2026-10-02T19:05:37.326Z", "operator": "alice" },
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": true,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "system",
"created_at": "2026-10-02T19:05:37.384Z",
"message_version": 0
},
{
"conversation_id": "99582580415791104",
"conversation_type": "group",
"seq": 2,
"message_id": "99582580872970240",
"client_msg_id": "evt_99582580453539840",
"sender": null,
"sender_type": "system",
"recipient": null,
"type": "tip",
"body": {
"kind": "members_joined",
"occurred_at": "2026-10-02T19:05:37.326Z",
"operator": null,
"members": ["bob", "carol"],
"joined_count": 2,
"joined_via": "create"
},
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": true,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "system",
"created_at": "2026-10-02T19:05:37.435Z",
"message_version": 0
}
]只推在线的消息
发送时设置 online_only 为 true 的消息只推给接收者此刻在线的设备,适合不需要保存的自定义信令,如“对方正在录音”、白板操作:
- 只能是
custom类型,不能带mentions、reply_to、need_receipt、exclude_from_unread和push; - 单聊推给接收者,群聊推给当前的群成员;服务端发送时按
sync_to_sender决定是否同时推给发送者的在线设备; - 不保存、不分配序号、不计入未读数,也不会创建会话;离线的设备收不到,之后也拉取不到;
- 不能按 ID 查询、撤回或编辑,也不去重:接收方请按“发送者 +
client_msg_id”自行去重; - 发送前的检查和限流与普通消息相同。
发送成功返回的是精简的结果,见发送消息。
消息保留期
服务端保存应用运行策略中 message_retention_days 天内的消息,默认 90 天,可设 1 到 3650 天,见运行策略;实际不超过应用套餐的消息保留天数,见套餐与账单。
- 超过保留期的消息不能再拉取、查询、导出,按 ID 查询返回
404 not_found;也不能再被引用、撤回、编辑和置顶,已置顶的随之移出置顶列表。过期的消息随后由后台任务删除。 - 会话本身、用户的已读位置和会话设置不随消息过期删除,两人很久以后再聊天仍是同一个会话。
- 消息中上传到本服务的附件另按
attachment_retention_days(默认 30 天,从上传时算起)保留。附件先到期时,消息仍在,下载附件时提示文件已过期,见附件的地址。 - 调短保留期不可撤销:调短后(包括更换为消息保留天数更短的套餐),更早的消息立即无法读取并随后被删除,再改回原值也不能恢复。
- 消息被撤回后内容立即清除,服务端也无法再查到原内容。需要留档的,请在撤回前通过导出或查询自行保存。
