卡片消息
卡片消息用 msgType: "card" 发送,是机器人最常用的富交互形式。发出去之后还可以原地更新或撤回,无需重发新消息。
- 主动发送:
POST /api/open/v2/bot/messages - 回复入站消息:
POST /api/open/v2/bot/messages/{messageId}/reply - 更新已发送卡片:
PUT /api/open/v2/bot/messages/{messageId}/card - 删除已发送卡片:
DELETE /api/open/v2/bot/messages/{messageId}/card
发送与回复所需 Scope 为 bot.message.send / bot.message.reply;更新与删除卡片所需 Scope 为 bot.message.send。
两种投递方式
content 里二选一:
| 方式 | content 结构 | 说明 |
|---|---|---|
| 内联卡片 | {"card": { ...卡片 JSON... }} | 卡片内容随消息一起提交,每次发送都要重复传 |
| 控制台模板 | {"cardId": "card_xxx"} | 引用控制台「卡片」页保存的模板,发送时才展开 |
- 两个字段都缺时返回
400+400006,文案为content.cardId不能为空。 cardId指向的模板必须属于当前应用,否则返回400+400006,文案为卡片模板不存在。- 用
cardId发送时,服务端会把模板内容展开后再校验,效果与内联一致。
卡片信封
卡片 JSON 必须满足以下结构,否则发送时直接失败:
{
"schema": "2.0",
"body": {
"elements": []
}
}
| 字段 | 要求 | 不满足时的文案 |
|---|---|---|
schema | 必须为字符串 "2.0" | 仅支持schema为2.0的卡片 |
body | 必须为 JSON 对象 | card.body不能为空 |
body.elements | 必须为 JSON 数组 | card.body.elements必须为数组 |
以上字段之外的卡片属性(如用于样式的 header 等)服务端不解析、原样透传,能否渲染以客户端的卡片能力为准。
组件白名单
elements 里的每个元素都必须是 JSON 对象,且 tag 必须在下列白名单内,否则返回 不支持的卡片组件: <你传的 tag>。
| 分组 | tag |
|---|---|
| 文本 | plain_text、text、markdown、lark_md、text_tag、text_tag_list |
| 布局与容器 | column_set、column、form、interactive_container、collapsible_panel、recycling_container |
| 分隔 | hr、divider |
| 图片与人员 | img、image、img_combination、icon、person、person_list |
| 交互 | button、overflow、action、input、textarea、checkbox、radio、select_static、select_person、select_time、date_picker、picker_datetime、multi_select_static、multi_select_person |
| 数据展示 | table、chart、note |
上表的分组仅为便于查阅,服务端只做「tag 是否在白名单内」这一项判断。白名单只保证组件类型被接受,不校验每种组件的必填字段——例如一个缺
content的text组件能通过服务端校验,但可能无法正常渲染。
嵌套容器
容器组件通过以下四个字段之一挂载子元素,服务端会递归校验子元素:
| 字段 | 服务端校验 | 客户端渲染 |
|---|---|---|
elements | ✅ | ✅ |
columns | ✅ | ✅ |
children | ✅ | ✅ |
actions | ✅ | ❌ 不读 |
校验按 elements → columns → children → actions 的顺序查找,只取第一个命中的字段;如果同层级同时写了多个,后面的不会被校验。
⚠️
actions是个坑,两端口径不一致。 服务端会校验actions,客户端却从不读取它。所以按飞书官方写法写的{"tag": "action", "actions": [...]}能通过校验、接口返回成功,客户端却渲染成一张空卡片。想让按钮组真的显示出来,子节点必须写进
elements:{"tag": "action","elements": [{ "tag": "button", "text": { "tag": "plain_text", "content": "通过" } }]}同理,其余容器(
form、interactive_container、collapsible_panel、recycling_container)也只会从elements取子节点,children只在客户端不认识的容器上兜底。
限制
| 限制 | 阈值 | 超出时的文案 |
|---|---|---|
| 组件总数 | ≤ 200 | 卡片组件不能超过200个 |
| 卡片 JSON 体积 | ≤ 64 KiB(UTF-8) | 卡片JSON不能超过64KiB |
| 嵌套层级 | 最深嵌套数组为第 9 层(body.elements 计第 1 层) | 卡片组件嵌套层级不能超过8 |
组件总数是递归统计的:容器本身与它的所有子孙节点各计 1。
嵌套层级的计数方式是「嵌套的数组层数」,不是组件个数,也不是容器个数。 body.elements 算第 1 层,
其下每出现一个子数组(elements/columns/children/actions)就再加一层,最多到第 9 层。
⚠️ 一个容易踩的坑:
column_set→columns→column→elements是两层数组, 所以「一层column_set」实际消耗两层额度。 实测边界:只用form(单层elements)时嵌 8 层通过、9 层报错; 用column_set/column组合时叠 4 层通过、5 层报错。
链接字段限制
以下四个字段只要出现,就必须是 http:// 或 https:// 开头的字符串,否则返回 卡片链接仅支持HTTP/HTTPS地址:
- 元素自身的
url、img_url、src action.url(元素action对象里的url)
字段缺失(null)视为不校验。非字符串类型同样会被拒绝——例如把 url 写成数字或对象,报的是同一条文案。
这条校验发生在展开图片之前,所以图片组件依赖
img_key而非img_url时(见下节)不会因为img_url缺失而报错。
图片组件
img / image 组件在同时缺少 img_url 与 url 时,服务端会尝试用 img_key 解析出图片地址:
- 取
img_key;缺失则返回card image img_key不能为空。 - 若
img_key本身是http:///https://地址,直接作为img_url使用。 - 否则当作上传接口返回的
fileKey解析(见 机器人快速接入 → 文件消息),解析成功且 MIME 类型以image/开头时,把解析出的地址写入img_url。
fileKey 的有效期约 10 分钟、且仅限上传它的应用使用,因此 img_key 必须在发送前临时上传获得,不能提前很久准备好。解析失败时分两种:
img_key不是有效fileKey(过期、跨应用、不存在)→403+400008,文案fileKey无效或已过期;img_key指向的文件 MIME 类型不以image/开头 →400+400006,文案card image img_key必须引用图片文件。
完整示例
curl -X POST 'https://im.lanchr.com/api/open/v2/bot/messages' \
-H 'Content-Type: application/json' \
-H 'X-Open-App-Id: <app-id>' \
-H 'X-Open-Credential-Id: <credential-id>' \
-H 'X-Open-Timestamp: <timestamp-ms>' \
-H 'X-Open-Nonce: <unique-nonce>' \
-H 'X-Open-Signature: <hmac-sha256>' \
--data-binary '{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "order-notice-20260917-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{ "tag": "markdown", "content": "**订单号** SO-20260917\n**金额** ¥128.00" },
{ "tag": "hr" },
{
"tag": "column_set",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "button",
"text": { "tag": "plain_text", "content": "查看详情" },
"action": { "type": "url", "url": "https://example.com/orders/SO-20260917" }
}
]
}
]
}
]
}
}
}
}'
成功响应(code 为 "000000"):
{
"code": "000000",
"message": "成功",
"value": {
"messageId": 8479,
"msgSeq": 8703555237400514949,
"clientMessageId": "order-notice-20260917-0001",
"duplicate": false
},
"reqNo": "req_34dee87a15bd4ae8a135e99c22b6bb4b"
}
拿到 value.messageId 后即可用于后续的更新与删除。
示例
下面每个示例都给出完整的请求体,替换 receiveId 就能直接调接口。截图取自蓝答桌面客户端的浅色主题,就是同一份 JSON 的实际渲染效果。
服务端只做结构校验,卡片长什么样完全由客户端决定。这些截图代表桌面客户端的渲染能力,换一个客户端可能长得不一样。校验规则见前面的组件白名单与限制。
每个示例下面的 ⚠️ 标注是实测踩过的坑——服务端会放行、客户端却不按预期渲染的情况,照着写之前先看一眼。
文本
纯文本
plain_text 渲染为一行普通文本,不支持任何标记语法。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-plain-text-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "plain_text",
"content": "服务将于今晚 23:00 进行例行维护,预计持续 30 分钟。"
}
]
}
}
}
}
plain_text与text等价,text是同一组件的别名。- 文案里的
**加粗**、[链接]()等标记不会被解析,会原样显示。
Markdown 富文本
markdown 支持加粗、斜体、链接、列表、行内代码等标记语法。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-markdown-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "markdown",
"content": "**订单号:** SO-20260922\n**金额:** ¥128.00\n\n- 商品:无线键盘\n- 数量:1\n- 收货人:张三"
}
]
}
}
}
}
markdown与lark_md等价,客户端用同一套渲染器处理。- 换行用
\n;空行分段。HTML 标签会被转义,不会执行。
状态标签
text_tag 渲染为一个小标签,用来标注级别或状态。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-text-tag-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "text_tag",
"text": {
"tag": "plain_text",
"content": "P1"
}
},
{
"tag": "plain_text",
"content": "数据库将在今晚进行维护。"
}
]
}
}
}
}
- 标签文案写在
text.content里;text_tag是独立元素,会单独占一行,不会和后面的文本排在同一行。 - 卡片标题栏里的
header.text_tag_list是另一个字段,用来在标题右侧挂多个标签。
布局与容器
多列并排
column_set 包住多个 column,把内容横向并排成多列。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-column-set-0001",
"content": {
"card": {
"schema": "2.0",
"header": {
"title": {
"tag": "plain_text",
"content": "服务概览"
}
},
"body": {
"elements": [
{
"tag": "column_set",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "plain_text",
"content": "在线用户\n1,280"
}
]
},
{
"tag": "column",
"elements": [
{
"tag": "plain_text",
"content": "今日消息\n8,642"
}
]
},
{
"tag": "column",
"elements": [
{
"tag": "plain_text",
"content": "错误率\n0.02%"
}
]
}
]
}
]
}
}
}
}
column_set的子节点必须放在columns里,不是elements。- 每个
column自己的子节点放在column.elements里。 - ⚠️ 这一层结构会消耗两层嵌套额度:
column_set→columns(第 1 层数组)→column→elements(第 2 层数组),详见限制里的嵌套层级说明。
单列容器
column 把多个组件归到同一列里,按垂直方向依次排列。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-column-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "column",
"elements": [
{
"tag": "plain_text",
"content": "版本 v2.6.0 已发布"
},
{
"tag": "markdown",
"content": "包含性能优化和安全修复。"
}
]
}
]
}
}
}
}
column可以直接放在body.elements里,不必非得套在column_set内。- 效果上它和不用容器差别不大,主要价值是在
column_set里划分列。
表单容器
form 用来承载可提交的表单控件。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-form-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "form",
"elements": [
{
"tag": "plain_text",
"content": "表单内容(静态展示)"
}
]
}
]
}
}
}
}
form的子节点放在elements里。- 客户端目前只做静态渲染,不会提交表单;表单控件的交互结果通过
feishu-card:action事件抛出。
操作按钮组
action 把多个按钮横向排列成一行。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-action-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "action",
"elements": [
{
"tag": "button",
"element_id": "approve",
"text": {
"tag": "plain_text",
"content": "通过"
},
"value": {
"action": "approve"
}
},
{
"tag": "button",
"element_id": "reject",
"text": {
"tag": "plain_text",
"content": "拒绝"
},
"value": {
"action": "reject"
}
}
]
}
]
}
}
}
}
- ⚠️ 子节点必须写在
elements里。服务端会依次查找elements→columns→children→actions,所以写成飞书官方的actions也能通过校验,但客户端只读elements,用actions会渲染成一张空卡片。
交互容器
interactive_container 用来组合需要整体参与交互的内容。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-interactive-container-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "interactive_container",
"elements": [
{
"tag": "plain_text",
"content": "交互容器内容(未启用回调)"
}
]
}
]
}
}
}
}
- 子节点放在
elements里。
折叠面板
collapsible_panel 把次要信息折叠起来,默认收起。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-collapsible-panel-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "collapsible_panel",
"title": {
"tag": "plain_text",
"content": "更多信息"
},
"elements": [
{
"tag": "plain_text",
"content": "折叠面板内容"
}
]
}
]
}
}
}
}
- 面板标题读
title(也兼容header),子节点放在elements里。 - ⚠️ 客户端不做折叠交互,标题和内容始终展开显示——想默认收起只能自己用
image之类做视觉暗示。
循环容器
recycling_container 用于按数据源循环渲染子项。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-recycling-container-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "recycling_container",
"elements": [
{
"tag": "plain_text",
"content": "循环项静态内容"
}
]
}
]
}
}
}
}
- 子节点放在
elements里。 - 客户端目前只把子项平铺渲染一次,不执行循环或数据绑定。
分隔
分割线
hr 渲染为一条水平分割线,用来分隔上下文。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-hr-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "plain_text",
"content": "维护窗口:2026-09-23 02:00 - 06:00"
},
{
"tag": "hr"
},
{
"tag": "plain_text",
"content": "维护期间消息推送可能延迟。"
}
]
}
}
}
}
hr与divider等价。
图片与人员
图片
image 渲染一张图片,可以用 img_key 或 img_url 指定地址。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-image-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "image",
"img_key": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png",
"alt": {
"tag": "plain_text",
"content": "欢迎横幅"
}
}
]
}
}
}
}
img_url、url、img_key三个字段客户端都会读,按这个顺序取第一个非空值。img_key给 http(s) 地址时直接当图片地址用;给fileKey时服务端会先解析(见图片组件)。alt是图片加载失败时的替代文本,不参与展示布局。
多图组合
img_combination 把多张图片横向排成一组。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-img-combination-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "img_combination",
"img_list": [
{
"img_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png",
"alt": {
"tag": "plain_text",
"content": "报表一"
}
},
{
"img_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png",
"alt": {
"tag": "plain_text",
"content": "报表二"
}
},
{
"img_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png",
"alt": {
"tag": "plain_text",
"content": "报表三"
}
}
]
}
]
}
}
}
}
- 图片列表放在
img_list里,每项支持img_url/url/img_key。 - 图片地址为空或加载失败的项会退化成默认头像,不会留空位。
单个人员
person 渲染一个人的头像和名字。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-person-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "person",
"user_id": "u_1001",
"name": {
"tag": "plain_text",
"content": "张三"
},
"avatar_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png"
}
]
}
}
}
}
- 显示名依次读
name→user_name→user_id,所以name不传也能渲染出user_id。 - 头像读
avatar_url(也兼容avatar);不传或地址非法时用默认头像。
人员列表
person_list 横向排列多个人。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-person-list-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "person_list",
"persons": [
{
"user_id": "u_1002",
"name": {
"tag": "plain_text",
"content": "李四"
},
"avatar_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png"
},
{
"user_id": "u_1003",
"name": {
"tag": "plain_text",
"content": "王五"
},
"avatar_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png"
}
]
}
]
}
}
}
}
- 人员数组放在
persons里,每项字段与单个person相同。
图标
icon 用来展示一个标准图标。
![]()
{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-icon-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "icon",
"token": "info_outlined"
}
]
}
}
}
}
- ⚠️ 客户端目前没有图标字体,
token会被当纯文本原样显示(截图里显示的就是info_outlined字样)。想稳定呈现图标,建议改用image传图片。
交互
链接按钮
button 渲染为可点击按钮,带 url 时点击直接打开链接。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-button-0001",
"content": {
"card": {
"schema": "2.0",
"header": {
"title": {
"tag": "plain_text",
"content": "✅ 构建成功"
}
},
"body": {
"elements": [
{
"tag": "markdown",
"content": "**分支:** master\n**提交:** abc123f\n**耗时:** 3m 24s"
},
{
"tag": "hr"
},
{
"tag": "plain_text",
"content": "测试覆盖率:92.5% | 代码扫描:通过"
},
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "查看构建详情"
},
"url": "https://ci.example.com/builds/1234"
}
]
}
}
}
}
- 按钮文案读
text.content。 url只接受http:///https://,否则发送时报卡片链接仅支持HTTP/HTTPS地址。- 不带
url时按钮走回调:客户端抛出feishu-card:action事件,value原样带出。
表单控件
input / textarea / checkbox / radio / select_static 渲染成可操作的控件。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-form-controls-0001",
"content": {
"card": {
"schema": "2.0",
"header": {
"title": {
"tag": "plain_text",
"content": "工单处理"
}
},
"body": {
"elements": [
{
"tag": "input",
"name": "assignee",
"placeholder": {
"tag": "plain_text",
"content": "处理人"
}
},
{
"tag": "select_static",
"name": "priority",
"placeholder": {
"tag": "plain_text",
"content": "优先级"
},
"options": [
{
"text": {
"tag": "plain_text",
"content": "高"
},
"value": "high"
},
{
"text": {
"tag": "plain_text",
"content": "中"
},
"value": "medium"
},
{
"text": {
"tag": "plain_text",
"content": "低"
},
"value": "low"
}
]
},
{
"tag": "textarea",
"name": "remark",
"placeholder": {
"tag": "plain_text",
"content": "处理说明"
}
},
{
"tag": "checkbox",
"name": "notify",
"label": {
"tag": "plain_text",
"content": "完成后通知我"
}
},
{
"tag": "radio",
"name": "close",
"label": {
"tag": "plain_text",
"content": "同时关闭工单"
}
}
]
}
}
}
}
- 控件的提示文案依次读
label→placeholder→name,都取不到时显示「请选择」。 select_*/overflow/date_picker等下拉类组件,客户端统一渲染成原生下拉框,选项读options[].text。- ⚠️
checkbox/radio的文案客户端读的是label(不是飞书原生的text),用text会显示成name或「请选择」。
数据展示
表格
table 渲染为只读表格。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-table-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "table",
"rows": [
[
"检查项",
"结果"
],
[
"单元测试",
"通过"
],
[
"代码扫描",
"通过"
]
]
}
]
}
}
}
}
rows用二维数组,第一行会被当作表头。- 也支持
columns定义列头,行数据可用对象形式;单元格里的对象会被取content显示。
图表
chart 用来展示一组数值。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-chart-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "chart",
"title": {
"tag": "plain_text",
"content": "消息趋势"
},
"data": [
{
"name": "成功",
"value": 80
},
{
"name": "失败",
"value": 20
}
]
}
]
}
}
}
}
- ⚠️ 客户端不执行图表脚本,只把
data里的前 8 项渲染成横向数据条,value被夹到 0–100 之间当作条宽。 data也兼容{ values: [] }/{ data: [] }/{ series: [] }等写法。
备注
note 渲染为一行次要说明文字。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-note-0001",
"content": {
"card": {
"schema": "2.0",
"body": {
"elements": [
{
"tag": "note",
"content": "这是通知卡片的补充说明。"
}
]
}
}
}
}
note的文案读content(也兼容text),样式上比正文更淡。
卡片级配置
标题栏
header 提供卡片标题、副标题、图标和标题标签。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-header-0001",
"content": {
"card": {
"schema": "2.0",
"config": {
"locales": [
"zh_cn"
]
},
"header": {
"title": {
"i18n": {
"zh_cn": "多语言通知"
}
},
"subtitle": {
"tag": "plain_text",
"content": "2026-09-22 09:00 生成"
},
"icon": {
"img_url": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png"
},
"text_tag_list": [
{
"text": {
"tag": "plain_text",
"content": "P1"
}
}
]
},
"body": {
"elements": [
{
"tag": "plain_text",
"content": "标题栏支持图标与多个标签。"
}
]
}
}
}
}
- 标题读
title.content;也支持title.i18n.{locale},此时要同时在config.locales里声明语言,客户端按顺序取第一个命中的。 subtitle显示在标题下方。icon支持img_url(图片)或tag: "standard_icon"+token(标准图标)。text_tag_list是数组,会在标题右侧并排渲染多个标签。
宽度与摘要
config 控制卡片宽度和会话列表里的摘要文案。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-config-width-0001",
"content": {
"card": {
"schema": "2.0",
"config": {
"width_mode": "fill",
"summary": {
"content": "主分支构建成功 #1234"
}
},
"body": {
"elements": [
{
"tag": "plain_text",
"content": "width_mode 为 fill 时卡片占满可用宽度。"
}
]
}
}
}
}
width_mode取值default(min(400px, 68vw))、compact(同 default)、fill(min(600px, 100%))。summary.content是会话列表里显示的摘要,不传则用卡片正文。
居中显示
config.horizontal_align 为 center 时卡片脱离气泡、在会话中居中占满整行。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-config-center-0001",
"content": {
"card": {
"schema": "2.0",
"config": {
"horizontal_align": "center",
"width_mode": "fill"
},
"header": {
"title": {
"tag": "plain_text",
"content": "消息助手通知"
}
},
"body": {
"elements": [
{
"tag": "plain_text",
"content": "这是一条居中显示的飞书卡片消息。"
}
]
}
}
}
}
- 这是唯一会改变卡片整体布局的配置:命中后不再显示头像和发送者昵称,卡片自己占满一行。
- 客户端依次检查
config.horizontal_align→config.align→align,任一为center即生效。 - ⚠️ 布局变化只在客户端发生,服务端不解析这些字段,也不会因为写错而报错。
卡片级链接
card_link 在卡片底部追加一个跳转入口。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-card-link-0001",
"content": {
"card": {
"schema": "2.0",
"card_link": {
"url": "https://status.example.com"
},
"body": {
"elements": [
{
"tag": "plain_text",
"content": "点击卡片链接查看完整状态页"
}
]
}
}
}
}
card_link.url只接受http:///https://。- 它和元素级的
url不是一回事:card_link是卡片级字段,渲染在卡片底部,与body.elements无关。
综合示例
系统维护公告
标题 + 分割线 + Markdown 列表 + 链接按钮的常见组合。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-system-notice-0001",
"content": {
"card": {
"schema": "2.0",
"config": {
"summary": {
"content": "系统维护通知"
}
},
"header": {
"title": {
"tag": "plain_text",
"content": "🔧 系统维护通知"
}
},
"body": {
"elements": [
{
"tag": "plain_text",
"content": "维护时间:2026年8月30日 02:00 - 06:00(约4小时)"
},
{
"tag": "hr"
},
{
"tag": "markdown",
"content": "**维护内容:**\n- 数据库性能优化\n- 消息推送服务升级\n- 安全补丁更新"
},
{
"tag": "plain_text",
"content": "维护期间部分功能可能暂时不可用,敬请谅解。"
},
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "查看详情"
},
"url": "https://status.example.com/maintenance/20260830"
}
]
}
}
}
}
入群欢迎
带图片的欢迎卡片,适合机器人进群后的第一条消息。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-welcome-0001",
"content": {
"card": {
"schema": "2.0",
"config": {
"summary": {
"content": "欢迎加入团队!"
}
},
"header": {
"title": {
"tag": "plain_text",
"content": "🎉 欢迎加入"
}
},
"body": {
"elements": [
{
"tag": "markdown",
"content": "欢迎来到我们的团队!\n\n为了让您快速上手,我们准备了以下资源:"
},
{
"tag": "hr"
},
{
"tag": "plain_text",
"content": "📘 新人指南 | 💬 团队规范 | 🛠️ 开发文档"
},
{
"tag": "image",
"img_key": "https://limp.oss-cn-hangzhou.aliyuncs.com/resources/avatar/avatar10.png",
"alt": {
"tag": "plain_text",
"content": "欢迎横幅"
}
},
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "查看新人指南"
},
"url": "https://wiki.example.com/onboarding"
}
]
}
}
}
}
审批通知
用 column_set 排出「字段 / 值」两列,再配一行操作按钮。

{
"receiveIdType": "group_id",
"receiveId": "<群ID>",
"msgType": "card",
"clientMessageId": "card-approval-0001",
"content": {
"card": {
"schema": "2.0",
"config": {
"summary": {
"content": "请假申请待审批"
}
},
"header": {
"title": {
"tag": "plain_text",
"content": "📋 请假申请待审批"
}
},
"body": {
"elements": [
{
"tag": "column_set",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "plain_text",
"content": "申请人"
},
{
"tag": "plain_text",
"content": "请假类型"
},
{
"tag": "plain_text",
"content": "请假时长"
}
]
},
{
"tag": "column",
"elements": [
{
"tag": "plain_text",
"content": "张三"
},
{
"tag": "plain_text",
"content": "年假"
},
{
"tag": "plain_text",
"content": "2026-09-24 ~ 2026-09-25(2 天)"
}
]
}
]
},
{
"tag": "hr"
},
{
"tag": "note",
"content": "提交时间:2026-09-22 09:12"
},
{
"tag": "action",
"elements": [
{
"tag": "button",
"element_id": "approve",
"text": {
"tag": "plain_text",
"content": "通过"
},
"value": {
"action": "approve",
"requestId": "LV-20260922-001"
}
},
{
"tag": "button",
"element_id": "reject",
"text": {
"tag": "plain_text",
"content": "拒绝"
},
"value": {
"action": "reject",
"requestId": "LV-20260922-001"
}
}
]
}
]
}
}
}
}
更新已发送的卡片
PUT /api/open/v2/bot/messages/{messageId}/card
请求体:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
card | object | 是 | 新的完整卡片 JSON,校验规则与发送时相同 |
version | integer | 否 | 乐观锁版本号;省略时不校验版本 |
只有本应用发出的卡片消息(direction=OUTBOUND)才能更新,否则返回 403 + 400008,文案为 消息未投递给此应用;目标不是卡片消息时返回 400 + 400006,文案为 卡片消息不存在。
版本号语义:
- 卡片的当前版本记录在消息的扩展信息里。内联卡片(
content.card)初始版本为1;用模板(content.cardId)发送时,初始版本为该模板当时的版本号。 - 每次成功更新后版本号 +1。
- 请求带了
version且与当前版本不一致时,更新会被拒绝。 - 不带
version时不做版本校验,直接覆盖,适合「每次都是全量重算卡片」的场景。 - 并发更新、或「读取 → 展示给用户 → 用户点击提交」这类跨请求的场景,建议始终带上
version。
⚠️ 版本冲突返回
HTTP 500+code 500000,且响应文案是通用的「服务内部错误,请稍后重试」,不是409,也拿不到「版本过期」这类提示。客户端无法从响应区分「版本冲突」与真正的平台内部错误。做乐观锁时请这样处理:收到500000后重新读取卡片当前状态再决定是否重试,不要直接向用户报「系统故障」。
更新成功后返回与发送相同结构的响应,但 clientMessageId 固定为 null、duplicate 固定为 false:
{
"code": "000000",
"message": "成功",
"value": {
"messageId": 8479,
"msgSeq": 8703555237400514949,
"clientMessageId": null,
"duplicate": false
},
"reqNo": "req_60c3253bf8294be18de23fb21b017fec"
}
更新会实时同步到已收到该消息的会话成员。
删除已发送的卡片
DELETE /api/open/v2/bot/messages/{messageId}/card
无请求体。删除即撤回该卡片,返回 value: true;若撤回未生效返回 value: false。前置校验与更新一致(必须是本应用发出的卡片消息,否则 403 + 400008 / 400 + 400006)。
⚠️ 服务端不记录撤回状态:对已删除的卡片再次调用
PUT .../card仍会返回成功,而不会报错。如果业务上「撤回」是终态,请在应用侧自行拦截,不要依赖服务端拒绝。
相关错误文案
| 文案 | HTTP | code |
|---|---|---|
仅支持schema为2.0的卡片 | 400 | 400006 |
card.body不能为空 | 400 | 400006 |
card.body.elements必须为数组 | 400 | 400006 |
卡片组件必须为对象 | 400 | 400006 |
不支持的卡片组件: xxx | 400 | 400006 |
卡片组件不能超过200个 | 400 | 400006 |
卡片JSON不能超过64KiB | 400 | 400006 |
卡片组件嵌套层级不能超过8 | 400 | 400006 |
卡片链接仅支持HTTP/HTTPS地址 | 400 | 400006 |
content.cardId不能为空 | 400 | 400006 |
卡片模板不存在 | 400 | 400006 |
card image img_key不能为空 | 400 | 400006 |
card image img_key必须引用图片文件 | 400 | 400006 |
卡片消息不存在 | 400 | 400006 |
fileKey无效或已过期 | 403 | 400008 |
消息未投递给此应用 | 403 | 400008 |
服务内部错误,请稍后重试(版本冲突时) | 500 | 500000 |
上表除最后一行外,文案都会原样返回在 message 字段里。版本冲突是唯一的例外:它走通用错误兜底,message 只有 服务内部错误,请稍后重试,看不出真实原因。
错误响应体统一为 ApiResult 信封,详见鉴权与签名 → 错误响应契约。