WhatsApp 互动消息
互动消息是正文加上一个可供收件人点击的元素:一个 WhatsApp 按钮、一个菜单、一个链接、一张卡片,或者一个请求收件人提供位置或联系方式的提示。如果模板回复意味着解析自由文本,那么一个 WhatsApp 菜单或一组 WhatsApp 按钮可以为收件人提供固定选项,并将你定义的值返回给你。本页介绍六种类型的共同之处;每种类型各自的页面介绍其传输格式和专属限制。
六种类型
| 类型 | Bird interactive.type | 头部 | 底部 | 正文上限 |
|---|---|---|---|---|
| 回复按钮 | button | 文本、图片、视频、文档 | 是 | 1024 |
| 列表菜单 | list | 仅文本 | 是 | 4096 |
| 链接按钮 | cta_url | 文本、图片、视频、文档 | 是 | 1024 |
| 媒体轮播 | carousel | 消息无头部;每张卡片可含图片或视频 | 否 | 消息 1024,每张卡片 160 |
| 位置请求 | location_request_message | 无 | 否 | 1024 |
| 联系方式请求 | request_contact_info | 无 | 否 | 1024 |
所有类型均为自由格式:只能在开放的客服窗口内发送,Meta 不会像审核模板那样对其进行审核。
互动消息属于自由格式内容,因此受客服窗口规则约束:参阅客服窗口了解其含义以及窗口关闭时的返回结果。
每次互动发送还需要 from,即你的工作区拥有的号码。Bird 的托管号码无法用于此目的,因此互动发送需要先接入一个你自己的号码。
互动内容字段
interactive 是 POST /v1/whatsapp/messages 上互斥的内容字段之一,与 template、text、image 等并列:一次发送中只能存在其中之一。在 interactive 内部,type 指明这是六种变体中的哪一种,该变体自身的字段承载其余数据(buttons、list、cta_url 或 cards)。Schema 禁止出现其他变体的字段,因此在一次发送中混用两种变体会在到达处理器之前就验证失败。
关于请求信封、202 响应模型和安全重试,请参阅发送 WhatsApp 消息,本页不再重复介绍。
以下是一条最简互动消息:在一次回复按钮发送中放置两个 WhatsApp 按钮,每次一种语言。
const msg = await bird.whatsapp.send({
to: "+15551234567",
from: "+13124495648",
interactive: {
type: "button",
body_text: "Your gardening workshop is scheduled for 9am tomorrow.",
buttons: [
{ type: "quick_reply", quick_reply: { slug: "change-booking", text: "Change" } },
{ type: "quick_reply", quick_reply: { slug: "cancel-booking", text: "Cancel" } },
],
},
});
console.log(msg.id, msg.status);msg = client.whatsapp.send(
to="+15551234567",
from_="+13124495648",
interactive={
"type": "button",
"body_text": "Your gardening workshop is scheduled for 9am tomorrow.",
"buttons": [
{"type": "quick_reply", "quick_reply": {"slug": "change-booking", "text": "Change"}},
{"type": "quick_reply", "quick_reply": {"slug": "cancel-booking", "text": "Cancel"}},
],
},
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+15551234567",
From: "+13124495648",
Interactive: &bird.WhatsAppInteractiveSend{
Type: "button",
BodyText: "Your gardening workshop is scheduled for 9am tomorrow.",
Buttons: &[]bird.WhatsAppInteractiveButtonSend{
{Type: "quick_reply", QuickReply: &bird.WhatsAppInteractiveQuickReplyButtonSend{Slug: "change-booking", Text: "Change"}},
{Type: "quick_reply", QuickReply: &bird.WhatsAppInteractiveQuickReplyButtonSend{Slug: "cancel-booking", Text: "Cancel"}},
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$interactive = (new WhatsAppMessageSendRequestInteractive())
->setType('button')
->setBodyText('Your gardening workshop is scheduled for 9am tomorrow.')
->setButtons([
(new WhatsAppInteractiveButtonSend())
->setType('quick_reply')
->setQuickReply((new WhatsAppInteractiveButtonSendQuickReply())->setSlug('change-booking')->setText('Change')),
(new WhatsAppInteractiveButtonSend())
->setType('quick_reply')
->setQuickReply((new WhatsAppInteractiveButtonSendQuickReply())->setSlug('cancel-booking')->setText('Cancel')),
]);
$message = $bird->whatsapp->send(
to: '+15551234567',
from: '+13124495648',
interactive: $interactive,
);
echo $message->getId(), ' ', $message->getStatus();bird whatsapp send \
--from +13124495648 \
--interactive '{"body_text":"Your gardening workshop is scheduled for 9am tomorrow.","buttons":[{"quick_reply":{"slug":"change-booking","text":"Change"},"type":"quick_reply"},{"quick_reply":{"slug":"cancel-booking","text":"Cancel"},"type":"quick_reply"}],"type":"button"}' \
--to +15551234567{
"name": "whatsapp_send",
"arguments": {
"from": "+13124495648",
"interactive": {
"body_text": "Your gardening workshop is scheduled for 9am tomorrow.",
"buttons": [
{
"quick_reply": {
"slug": "change-booking",
"text": "Change"
},
"type": "quick_reply"
},
{
"quick_reply": {
"slug": "cancel-booking",
"text": "Cancel"
},
"type": "quick_reply"
}
],
"type": "button"
},
"to": "+15551234567"
}
}curl -X POST "https://{region}.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+15551234567",
"from": "+13124495648",
"interactive": {
"type": "button",
"body_text": "Your gardening workshop is scheduled for 9am tomorrow.",
"buttons": [
{ "type": "quick_reply", "quick_reply": { "slug": "change-booking", "text": "Change" } },
{ "type": "quick_reply", "quick_reply": { "slug": "cancel-booking", "text": "Cancel" } }
]
}
}'按钮
六种类型中有四种包含按钮,它们都使用相同的结构:一个带判别字段的对象,其 type 为 quick_reply 或 cta_url,各自携带同名的嵌套字段。quick_reply 按钮携带 slug 和 text;cta_url 按钮携带 text 和 url。各类型接受的按钮形式如下:
- 回复按钮仅发送 quick_reply 按钮,1 到 3 个。
- 链接按钮只发送一个 cta_url 按钮。
- 媒体轮播在每张卡片上放置按钮:一个 cta_url 按钮,或最多三个 quick_reply 按钮,且轮播中每张卡片必须一致。
- 列表菜单使用分区内的行而非此按钮对象,详见其专属页面。
quick_reply 按钮的 slug 是你为该按钮设置的标识。它不会展示给收件人,收件人看到的只有它的 text 标签,而 slug 会在回复中原样回传。正是这个往返过程使回复可以关联到产生它的按钮,因此在这里统一说明一次,而不在每个子页面重复。
读取回复
点击按钮或选择菜单行会发送一条入站消息,携带一个 interactive_reply 对象。interactive_reply.type 为 button 或 list;无论是哪个,嵌套对象都携带你声明的 slug 和 text(即收件人实际看到的已点击标签)。两种请求类型(位置请求和联系方式请求)的回复方式不同:位置请求的回复是一条普通的入站位置消息,联系方式请求的回复是一条入站联系人名片,而非 interactive_reply。
回复通过消息列表和 GET /v1/whatsapp/messages/{id} 到达你,与任何入站 WhatsApp 消息的方式相同。要在回复到达时立即处理而非轮询,请订阅 whatsapp.received webhook:其载荷携带 interactive_reply,因此已包含被点击的按钮或行。接收互动回复介绍了点击的读取结构、webhook 载荷以及到达其他字段的点击。
引用消息以关联回复
发送时的 in_reply_to_message_id 引用同一会话中的一条早期消息,每条消息(无论发送还是接收)在读取时都会回传该字段。它是双向共用的同一个字段。
此关联是非对称的。点击 WhatsApp 按钮或菜单行时会携带 Meta 自有的 context,因此 in_reply_to_message_id 可解析到提供该选项的消息。共享的联系人名片不携带 context,因此无法解析:你需要通过 from 和时间来关联联系方式请求的回复,而非依赖此字段。
解析通过消息上下文存储进行,未命中时会省略该字段而非返回一个值。这在传输层上与完全没有回复目标的回复无法区分。如果集成需要可靠的关联,不应仅依赖此字段:在发送时携带你自己的 metadata,然后基于它进行匹配。
消息可被引用的窗口限制为 15 天;超过后发送会返回 404 E15071,因为 Bird 不再持有引用所需的提供方 ID。发送 WhatsApp 消息负责发送端字段的说明:其长度、解析方式和请求结构。
错误
三个错误码专用于互动内容。每个仅在包含其所检查字段的类型上触发,因此第四列标明了哪些类型会实际遇到该错误。
| 错误码 | 状态 | 触发条件 | 适用于 |
|---|---|---|---|
| E15055 WhatsAppInteractiveLimitExceeded | 422 | 消息超出其类型的限制;目前为列表各分区合计超过 10 行。 | 仅列表菜单 |
| E15056 WhatsAppInteractiveDuplicateLabel | 422 | 同一消息中两个按钮或行使用了相同的标签。 | 任何包含带标签按钮或行的类型:回复按钮、列表菜单、媒体轮播 |
| E15059 WhatsAppInteractiveCarouselButtonsMismatch | 422 | 轮播中的卡片未全部携带相同的按钮。 | 仅媒体轮播 |
每次互动发送也可能遇到任何 WhatsApp 发送都会遇到的错误:客服窗口已关闭、发送方缺失或无效、收件人无效或内容不明确。这些错误在所有 WhatsApp 内容类型中通用,并非互动消息特有;参阅发送 WhatsApp 消息获取完整列表,此处不再重复。
后续步骤
- 发送 WhatsApp 消息:请求信封、202 模型和安全重试
- WhatsApp 事件:通过 API 或 webhook 跟踪每条消息的投递状态
- WhatsApp 模板:窗口关闭后仍可发送的消息