Sign inGet Started

发送 WhatsApp 消息

本指南介绍发送端点 POST /v1/whatsapp/messages。你构造一个 JSON 载荷,包含一个收件人和恰好一种内容类型:预审批的模板,或承载文本、图片、视频、音频、贴纸、文档、位置、联系人名片或可点击内容的服务消息。Bird 返回 202 Accepted(附带消息 ID)并异步投递。你能发送哪种类型取决于客服窗口。每个请求向一个收件人发送一条消息,没有批量端点。

最小发送示例

最小的有效载荷包含一个 to 收件人和一个带 slugtemplate。如果需要指定语言,添加 language;省略则使用模板的默认语言。通过 components 填充模板声明的所有变量。
curl 调用指定的是美国主机;如果你的密钥以 bk_eu1_ 开头,请改为调用 https://eu1.platform.bird.com。SDK 会从密钥中读取区域,因此无需设置主机。
const msg = await bird.whatsapp.send({
  to: "+15551234567",
  template: {
    slug: "bird_otp",
    components: [{ type: "body", parameters: [{ type: "text", text: "123456" }] }],
  },
});
console.log(msg.id, msg.status);

客服窗口

你能发送哪种类型取决于一个状态:客服窗口是否处于打开状态。
联系人通过向你的企业号码发送消息或拨打电话来打开窗口,窗口保持 24 小时有效,每次联系人再次发送消息时重置。窗口打开期间,你可以发送服务消息,即任意自由格式内容:文本、图片、视频、音频、贴纸、文档、位置或互动消息。窗口关闭后,只有预审批的模板能送达对方,而对方回复模板会重新打开窗口。
Bird 为你跟踪窗口状态,因此向已关闭的窗口发送服务消息会在创建或计费之前被拒绝:请求返回 422 E15044 WhatsAppServiceWindowClosed。该检查为尽力而为且失败时默认放行,因此 202 并不能证明窗口在分发时确实处于打开状态;如果窗口在接受和分发之间关闭,则异步失败,消息的 last_error 上会出现 service_window_expired
参阅客服窗口了解完整生命周期:什么操作会打开窗口、什么操作会重置窗口,以及窗口如何影响定价。

构建载荷

收件人

to 是单个收件人,可以是电话号码或企业范围用户 ID。电话号码使用 E.164 格式:以 + 开头,接国家代码和用户号码,例如 +14155550100。我们会验证号码,因此不可能是真实可拨号码的值(长度错误、前缀未分配)会在计费之前被拒绝,返回 422 WhatsAppInvalidRecipient。一条消息发送给一个收件人;没有收件人数组也没有批量发送,因此向多人发送时需要每个收件人一次调用。
企业范围用户 ID(例如 US.13491208655302741918)用于联系你没有其电话号码的用户,适用于回复未提供号码的联系人。有两点不同:发送号码必须属于该 ID 所属的同一企业组合,且一次性验证码模板需要电话号码。Bird 托管的模板会在接受时被拒绝,返回 422 WhatsAppRecipientNotSupportedForTemplate;你的工作区自建的身份验证模板会被接受但随后失败,因为 Meta 要求提供电话号码。

模板

template 指定要发送的预审批模板:
  • slug(必填):模板的 slug,例如 bird_order_confirmation。必须与你目录中的模板匹配(小写字母、数字和下划线)。
  • language:模板的语言标签,例如 enpt-BR。省略则使用模板的默认语言;指定模板不支持的语言会返回 422,其中列出可用的语言。已接受的消息会回显解析后的语言。
  • components:填充模板变量的值(参阅组件和参数)。如果模板没有变量则省略。
模板页面浏览你的模板、可用语言以及每个模板的渲染预览。

组件和参数

模板包含变量,可以是命名的({{ref}}{{amount}})或编号的({{1}}{{2}})。你通过 components 提供它们的值。每个组件指定一个 typebodybutton)和一个 parameters 数组。每个参数指定自己的 typetextimagevideogifdocumentlocation),并携带对应的字段:text 为纯字符串,image/video/gif/document 为公开的 https urllocation 为地图上的坐标。使用命名参数的模板要求每个参数都包含 name,且必须与模板声明的名称完全匹配(参阅字段参考)。位置型模板省略 name,按 {{n}} 顺序取值,因此第一个参数填充 {{1}}。无论哪种方式,与模板声明不匹配的参数会返回 422 WhatsAppTemplateParameterMismatchheader 组件类型也存在于协议中:在 Bird 托管的模板上会被丢弃,因为没有 Bird 托管的模板声明头部变量;但在你的工作区自建的模板上会被转发,这就是媒体头部工具型或营销型模板获取图片的方式。
例如,一个一次性验证码模板,正文内容为 {{1}} is your verification code,按钮复制验证码,则需要将验证码同时作为正文参数和按钮参数传入,按位置方式(无 name):
代码示例
{
  "components": [
    { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] },
    { "type": "button", "parameters": [{ "type": "text", "text": "481920" }] }
  ]
}

类别和发送方

模板的类别(authenticationutilitymarketing)决定 WhatsApp 如何处理消息,并与目的地国家一起决定费用。
发送方的归属决定你是否需要指定它:
  • Bird 托管的模板(其 slug 以 bird_ 开头)使用 Bird 为该类别保留的号码发送,因此省略 from。设置该字段会返回 422 WhatsAppSenderNotAllowed
  • 其他所有情况from 中指定自己的发送方:任何类型的服务消息,以及你的工作区自建的任何模板。号码必须是你的工作区拥有的号码。省略会返回 422 WhatsAppSenderRequired,工作区无权使用的号码会返回 422 WhatsAppSenderNotFound。自建模板还必须与号码属于同一个 WhatsApp Business Account,否则发送会返回 422 WhatsAppSenderWABAMismatch
电话号码设置涵盖两种号码类型以及如何接入你自己的号码。

服务消息

不使用 template 时,携带 textimagevideoaudiostickerdocumentlocationcontact_cardsinteractive 中的恰好一个。这九种都是服务消息,因此需要打开的客服窗口。它们都要求 from 为你的工作区拥有的号码;Bird 的托管号码无法用于发送服务消息。
  • text{ "body": "..." },最多 4096 个字符。添加 "preview_url": true 可为 body 中的第一个 URL 渲染链接预览。
  • imagevideoaudiostickerdocument:各接受一个公开的 https URL,WhatsApp 在发送时拉取(url),因此签名 URL 的有效期必须长于发送耗时。http URL 会被直接拒绝。WhatsApp 拉取文件本身,因此无法访问的 URL、返回不支持类型的 URL 或超出该类型大小限制的文件会被接受但随后失败,消息的 last_error 上出现 media_rejected,WhatsApp 自身的原因在 description 中。imagevideodocument 还接受可选的 captiondocument 还接受可选的 filenameaudio 接受可选的 voice 标志以呈现语音备忘录。
  • location{ "latitude": ..., "longitude": ... }(均为必填,十进制度数)加上可选的 nameaddress
  • contact_cards:一条消息中最多共享五个联系人的数组。每张名片的 name 需要 formatted_name 加上至少另一个部分(first_namelast_namemiddle_nameprefixsuffix);phone_numbersemailsurlsaddresses 各最多十个条目,orgbirthday(作为 YYYY-MM-DD)是可选的。E.164 格式的 phone_number 会使该名片获得一个打开聊天的按钮。
  • interactive:正文文本加上可点击的内容,共六种类型之一:回复按钮、列表菜单、链接按钮、媒体轮播,或请求收件人提供位置或电话号码的单个按钮。互动消息涵盖每种类型的协议结构、点击产生的回复以及限制。
代码示例
{
  "to": "+16505551234",
  "from": "+13124495648",
  "text": { "body": "Your order shipped: https://example.com/track/A1B2C3", "preview_url": true }
}
不携带任何内容或携带多种类型的请求会被拒绝,返回 422

引用消息

in_reply_to_message_id 设置为 WhatsApp 消息 ID,即可将你的消息作为对该消息的回复发送,就像在 WhatsApp 应用中点击回复引用消息一样。收件人会看到你的消息上方显示被引用的消息,该字段在每次读取消息时都会返回。
反过来也一样:WhatsApp 标记为回复的入站消息在同一字段中携带被引用消息的 ID,这就是你判断对方回复了你哪条消息的方式。WhatsApp 未标记的入站消息不携带 ID,且解析也可能遗漏。如需可靠的关联,请使用显式的互动回复标识符,结合你应用中存储的会话或任务状态。出站的 metadata 保留在出站记录上,不会自动复制到回复中。
引用在发送被接受之前解析,因此无法渲染的引用会导致请求本身失败,不会创建或计费任何内容。指定本工作区不持有的消息 ID,或超过 15 天可引用期限的消息,返回 404 E15071 WhatsAppReferencedMessageNotFound。指定从未到达 WhatsApp 的消息,或与本次发送的 tofrom 不属于同一会话的消息,返回 422 E15072 WhatsAppMessageNotQuotable。如果 Bird 无法访问回答该查询的存储,发送返回 503 E15073 WhatsAppMessageLookupUnavailable,值得重试。引用适用于模板发送和自由格式发送。
代码示例
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "text": { "body": "Yes, that slot is still free." }
}

标签和元数据

两个可选字段用于将你自己的上下文附加到消息上;两者都会在 API 读取时返回,并随该消息的每个 webhook 事件一起传递:
  • tags:最多 20 个结构化的 { "name": ..., "value": ... } 标签,用于低基数维度的过滤和报告(如活动、实验变体)。名称和值接受 ASCII 字母、数字、下划线和连字符;名称最多 32 个字符且在一次发送中唯一,值最多 64 个字符。通过标签过滤消息列表(?tag=campaign?tag=campaign:launch-week),指标页面按标签细分投递数据。
  • metadata:一个任意的 JSON 对象,序列化后最多 2 KB,用于不需要作为过滤维度的单次发送上下文(如内部订单 ID、会话引用)。
代码示例
{
  "tags": [{ "name": "campaign", "value": "order-confirmations" }],
  "metadata": { "order_id": "ord_8271" }
}

字段参考

字段类型必填限制/说明
tostring每条消息一个收件人:E.164 电话号码,或企业范围用户 ID(一次性验证码模板不接受后者)
fromstring (E.164)否**Bird 托管的模板省略此字段,它会自行选择发送方;服务消息和你的工作区自建的模板必填,且号码必须是你的工作区拥有的
template.slugstring否**你的工作区可发送的模板 slug;Bird 托管的 slug 以 bird_ 开头
template.languagestring否*模板语言标签(enpt-BR);省略则使用模板的默认语言
template.componentsarray填充模板的变量;组件 typebodybutton
template.components[].parameters[].namestring否†此值填充的占位符,例如 ref;命名参数模板必填且必须与模板声明的名称匹配,位置型模板省略
interactiveobject否**正文文本加上一种可点击内容;属于服务消息,因此需要打开的服务窗口。参阅互动消息
in_reply_to_message_idstring本工作区持有的 WhatsApp 消息 ID,在你发送的消息中引用;读取时回显。参阅引用消息
tagsarray最多 20 个 {name, value} 标签;名称 ≤ 32 字符,值 ≤ 64,名称唯一
metadataobject任意 JSON,序列化后最多 2 KB
* language 是可选的;省略则使用模板的默认语言。 † name 在命名参数模板中每个参数都必填。位置型模板省略。参阅组件和参数。 ** 携带 template 或服务消息内容字段(textimagevideoaudiostickerdocumentlocationinteractive)中的恰好一个;参阅服务消息

异步模型:202 的含义

成功发送返回 202 Accepted,附带消息 ID 和 status: accepted202 仅在发送被持久接受后才返回;不会先接受后静默丢弃。可修复的硬性失败会立即返回 422:无效收件人、未知模板 slug 或语言、参数不匹配,或向已关闭的客服窗口发送服务消息(WhatsAppServiceWindowClosed)。钱包余额不足不在其中:发送被接受,当 Bird 尝试扣费时消息最终变为 rejected,错误码为 insufficient_balance。实际投递异步进行:消息在交付给 WhatsApp 时变为 sent,然后在回执到达时变为终态(deliveredfailed),通过 eventswebhooks 和读取端点报告。已读回执作为 read_at 时间戳和 whatsapp.read 事件单独呈现,而非作为状态。
一条隐私说明:对于 authentication 类别的模板,API 不会返回填充后的值。202 回显和之后的每次读取都会为这些消息携带空的 components 数组,因此验证码不会再次暴露。

安全重试

发送时在 Idempotency-Key 请求头中携带每次逻辑发送的唯一值,重试即可安全执行。如果你的首次请求已成功但你未收到响应(超时、连接中断),使用相同 key 重放请求会返回原始结果,而不会发送并收费一条重复消息。重放的响应携带 Idempotency-Replay 请求头。参阅幂等性了解 key 格式和保留期限。

接收回复

入站消息与出站消息位于同一资源上,每条入站消息都会重置服务窗口。接收 WhatsApp 消息涵盖通过 API 读取入站消息、获取联系人发送的媒体文件,以及 whatsapp.received webhook。

费用和计费

WhatsApp 按消息计费,基于模板类别和收件人所在国家;参阅 WhatsApp 定价。消息分两步在不同时间点计费,消息上的 cost 对象报告这两部分:
字段含义产生时间
transaction_amountBird 处理发送的费用Bird 处理已接受的发送时,分发之前
passthrough_amountMeta 的消息费用份额,由 Bird 透传当适用的 deliveredread 回执到达时
amount已计价组件的金额总和随每个组件到达而增长
currency_code你的组织钱包的币种,两个组件共用随第一个组件一起出现
两个金额均为十进制字符串,不含税。
两个组件基于不同的输入定价。Bird 的费用使用你发送的模板类别和收件人所在国家,国家来自电话号码的国家代码,或在发送给企业范围用户 ID时来自该 ID 的两字母前缀。Meta 的份额使用 Meta 自身在适用回执上报告的类别,该类别可能与模板的不同:当 Meta 的目的地、企业位置和资格规则适用时,Meta 可能报告 authentication-international。参阅 WhatsApp authentication-international 费率
cost 的读取值取决于消息到达了哪个阶段:
  • 202cost 为 null。尚未定价。
  • 处理之后transaction_amount 已设置且 amount 等于它。passthrough_amount 仍为 null。
  • 在适用的 deliveredread 回执之后,成功记录的 Meta 费用填充 passthrough_amountamount 反映已记录的组件。
null 组件表示该投影中没有记录金额;它不能证明该消息免费。显式定价为零的组件读取为 "0.00000"
两种费用的失败方式也不同。Bird 的费用采用关闭式失败:如果在 202 之后因钱包无法负担发送或路由没有配置价格而无法扣费,消息最终变为 rejected,错误码为 insufficient_balanceprice_not_found,不会产生任何费用。rejected 消息从未到达 WhatsApp,这就是它与 failed 的区别。Meta 的份额采用开放式失败:如果回执到达时钱包余额不足或费率缺失,扣费被跳过,不会回滚已观察到的消息状态。你的投递不会因第二笔费用而延迟。
Bird 已扣费的消息即使后来投递失败也会保留该出站费用。Meta 费用在 Meta 报告具有可解析类别和目的地的常规定价时,从适用的 deliveredread 回调中处理。两条回调路径使用相同的费用标识并依赖计费服务的去重。请将重放的回执与账单记录进行对账,而非将消息投影视为永久扣款凭证。服务类或免入场定价可使 Meta 组件为零;未解析的组件不能证明该消息免费。
请使用账单台账进行财务对账。消息的 cost 字段是费用的投影,可能滞后或不完整。参阅 WhatsApp 指标了解消息观察值与账单记录的区别。
WhatsApp 事件不携带费用信息。要读取任一组件,请使用 GET /v1/whatsapp/messages/{id} 读取消息。

后续步骤