Sign inGet started

WhatsApp 事件

Bird 为入站和出站 WhatsApp 消息记录事件。出站时间线显示发送返回 202 之后发生了什么:接受、移交给 WhatsApp、投递、已读或失败。入站时间线记录 Bird 何时收到消息。

事件信封

WhatsApp 投递、入站消息和回应事件使用标准 webhook 信封:一个 type、一个 timestamp,以及一个特定于类型的 data 对象。
代码示例
{
  "data": {
    "direction": "outbound",
    "from": { "phone_number": "+13124495569" },
    "metadata": { "session_id": "sess_4821" },
    "tags": [{ "name": "flow", "value": "login-otp" }],
    "to": { "phone_number": "+14155550100" },
    "whatsapp_id": "wam_01ky7qbvswf3fvyaw3az90391c",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
  },
  "timestamp": "2026-07-23T14:51:39.913Z",
  "type": "whatsapp.delivered"
}
每条消息的公共 WhatsApp webhook 载荷包含 whatsapp_idworkspace_iddirectionfromtotagsmetadatawhatsapp.reacted 是例外,因为回应是对消息的标注而非独立消息;下文回应部分给出了其结构。地址可以包含 E.164 格式的 phone_numberbsuid 中的 Meta 业务范围用户 ID,或两者兼有。从 WhatsApp 用户收到的消息还会携带该用户公开的资料,位于 usernamedisplay_name 中。当发送时未携带时,tagsmetadatanull。以回复形式发送的消息还会在其时间线的每个出站事件上携带 in_reply_to_message_id,从 whatsapp.acceptedwhatsapp.readwhatsapp.failedwhatsapp.rejected,指明其回复的消息。
事件 API 返回更精简的时间线记录,包含 idtypeoccurred_at 时间戳。消息 ID 已包含在请求 URL 中。

生命周期事件

事件按时间顺序排列。出站消息可能止于 whatsapp.failedwhatsapp.rejected,其 whatsapp.read 事件仅在收件人打开消息时才出现。入站时间线以 whatsapp.received 开始,在您的工作区将消息标记为已读后可记录 whatsapp.read
事件含义
whatsapp.acceptedBird 接受了发送请求。这是 202 报告的内容。
whatsapp.sentBird 已将消息移交给 WhatsApp 网络。
whatsapp.deliveredWhatsApp 确认消息已投递到收件人的设备。
whatsapp.read收件人打开了消息。
whatsapp.failed消息未投递。error.code 说明了阻止原因。
whatsapp.rejectedBird 在发送前拒绝了消息。未产生费用。
whatsapp.receivedBird 收到了来自联系人的入站消息。
适用的 deliveredread 回调可触发 Meta 的费用份额。WhatsApp 事件载荷不包含费用信息。通过 GET /v1/whatsapp/messages/{message_id} 回读消息以查看其费用。参见费用与计费
将入站消息标记为已读会在其时间线中记录 whatsapp.read,但不会触发已读确认 webhook。入站消息保持其 received 状态,并在 WhatsApp 接受确认后记录 read_at
whatsapp.read 不会更改消息的 status。已投递的消息保持 delivered;消息同时在 read_at 中记录已读。
whatsapp.delivered 可能被完全跳过。 当收件人的设备上已打开该聊天时,Meta 会报告已读而不报告投递,因此时间线显示为 whatsapp.acceptedwhatsapp.sentwhatsapp.read,中间没有 whatsapp.delivered。请将 read 视为投递证明:等待 delivered 才认为消息已送达的消费端,恰好会卡在最快看到消息的收件人上;仅根据 delivered 计算投递率会导致低报。在这种情况下,消息 status 保持 sent,因为只有投递回执才会推进该状态。
仅含已读的回调仍可触发适用的 Meta 费用。Bird 在投递和已读路径上使用同一费用标识;缺少投递事件并不意味着 Meta 部分免费。参见费用与计费
事件类型列表是开放的:新类型可能随时间增加,因此请将无法识别的值视为未来事件而非错误。

失败事件

whatsapp.failedwhatsapp.rejected 是终态。拒绝表示 Bird 在将消息发送到 WhatsApp 之前拦截了它,因此不会产生费用。原因包括被屏蔽或已退订的收件人、钱包余额不足或目的地未配置价格。失败表示消息未被投递,error.code 说明了是谁做出的判定。大多数错误码携带 WhatsApp 的裁定,映射自其报告的代码。internal_error 是例外:它记录缺少可用的发送方凭证或处理重试已耗尽。不确定的传输尝试并不能证明 Meta 从未收到该请求。meta_error_code 在可用时包含 WhatsApp 的代码,而 internal_error 失败按定义不包含任何代码。
两种事件都包含一个 error 对象,带有一个稳定的 Bird code、一个人类可读的 description、一个可选的 meta_error_code,以及 occurred_at。该对象仅在这些事件类型的 API 记录和 webhook 载荷中出现。

回应事件

表情回应是对已有消息的标注,不会创建 whatsapp.received 消息。Bird 在联系人添加、更改或移除回应时触发 whatsapp.reacted,详见回应。您的商业号码发送的回应不会触发该 webhook。参见发送回应以添加、替换或移除您的回应,以及接收回应获取 webhook 和 REST API 示例。联系人的回应不会打开客服窗口。
被回应消息的回应日志记录联系人和您的商业号码双方的变更:添加、替换和移除。
有一种情况不会被记录。Bird 通过一个保留 15 天的提供商 ID 将回应与消息匹配,而 WhatsApp 允许对最多 30 天前的消息进行回应,因此对超过该期限的消息的回应无法被匹配,既不会出现在日志中也不会出现在 reactions 中。消息没有条目并不能证明没有人对其做过回应。
使用 GET /v1/whatsapp/messages/{message_id}/reaction-events 读取该日志,按最新排列。每个条目包含表情、变更者,以及 receivedsentfailedrejectedstatusfailedrejected 条目在 error 上携带原因。每个条目有一个回应 ID(war_…)和一个 occurred_at 时间戳。移除操作包含 emoji: null。待处理的变更在结果确定前没有条目。回应永远不会产生费用,因此其中的失败不涉及计费。如果需要查看消息上当前生效的回应而非变更历史,请使用 GET /v1/whatsapp/messages/{message_id} 读取其 reactions,它会将日志折叠为每个发送者一个条目。

屏蔽事件

除了逐条消息的生命周期外,还有一个事件报告工作区屏蔽列表的变更:whatsapp_suppression.created 在屏蔽生效时触发。载荷包含 suppression_id、E.164 格式的被屏蔽 address、该屏蔽所限定的 waba(为 null 时表示覆盖整个工作区,无论哪个帐号发送)、reasonworkspace_id,您的系统无需轮询即可获知新的屏蔽。只有屏蔽生效会触发事件;屏蔽结束目前不会触发,因此在将已镜像的屏蔽视为仍然有效之前,请重新读取列表:
代码示例
{
  "type": "whatsapp_suppression.created",
  "timestamp": "2026-08-25T14:52:03.192524705Z",
  "data": {
    "suppression_id": "was_01krdgeqcxet5s7t44vh8rt9mg",
    "address": "+14155550100",
    "waba": null,
    "reason": "manual",
    "workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf"
  }
}
收件人自行声明的退订是偏好而非屏蔽,触发的是 preference.revoked

从 API 读取事件

GET /v1/whatsapp/messages/{message_id}/events 按时间顺序返回时间线。该有界列表不分页。读取事件需要一个具有 whatsapp:read 权限的 API 密钥:
const { data } = await bird.whatsapp.listEvents("wa_abc123");
for (const event of data) console.log(event.type, event.occurred_at);
一条被接受、发送、投递并已读的消息会返回四个事件:
代码示例
{
  "data": [
    {
      "id": "ev_01ky7q6a1fejfbvs0myn41hj41",
      "occurred_at": "2026-07-23T14:48:34.71Z",
      "type": "whatsapp.accepted"
    },
    {
      "id": "ev_01ky7q6a2denvtd6jg1vqwmg13",
      "occurred_at": "2026-07-23T14:48:35.671Z",
      "type": "whatsapp.sent"
    },
    {
      "id": "ev_01ky7q6a2zff9r2qm74mmg1g6z",
      "occurred_at": "2026-07-23T14:48:36.642Z",
      "type": "whatsapp.delivered"
    },
    {
      "id": "ev_01ky7q6c21frssf0vj8h50qysw",
      "occurred_at": "2026-07-23T14:48:38.65Z",
      "type": "whatsapp.read"
    }
  ]
}
传入 type 可返回一个确切的公开事件类型,例如 ?type=whatsapp.failed?type=whatsapp.read。省略则返回完整时间线。
该时间线与您打开消息时 WhatsApp 日志页面所呈现的内容相同。
Bird 仪表盘中的 WhatsApp 消息详情面板,打开了一条已投递的 bird_order_confirmation 消息:Events 选项卡显示逐条消息的生命周期时间线(Accepted、Sent、Delivered 和 Read),各自带有时间戳,背景是变暗的消息列表

Webhooks

Webhooks 页面或 webhooks API 中订阅 whatsapp.acceptedwhatsapp.sentwhatsapp.deliveredwhatsapp.readwhatsapp.failedwhatsapp.rejectedwhatsapp.receivedwhatsapp.reactedWebhooks 指南涵盖端点、签名和重试。
whatsapp.received 在上述信封之上携带消息内容,因此端点无需回读即可对入站消息执行操作。对交互式消息的点击以 interactive_reply 形式到达,in_reply_to_message_id 指明其所回复的消息:
代码示例
{
  "data": {
    "direction": "inbound",
    "from": {
      "display_name": "Alex Rivera",
      "phone_number": "+14155550100",
      "username": "alexr"
    },
    "in_reply_to_message_id": "wam_01ky7qbvswf3fvyaw3az90391c",
    "interactive_reply": {
      "list": {
        "description": "Next day to 2 days",
        "slug": "priority_express",
        "text": "Priority Mail Express"
      },
      "type": "list"
    },
    "metadata": null,
    "tags": null,
    "to": { "phone_number": "+13124495569" },
    "whatsapp_id": "wam_01ky8b3xq4gd7pmzn2ka51f7te",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
  },
  "timestamp": "2026-07-23T14:52:04.118Z",
  "type": "whatsapp.received"
}
其他内容分支遵循相同的多选一结构:textimagevideoaudiostickerdocumentlocationcontact_cards,以及 unsupported(用于 API 未建模的类型)。GET /v1/whatsapp/messages/{message_id} 记录了每一种。

回应

whatsapp.reacted 在 WhatsApp 用户对您的一条消息做出回应时触发。它是唯一不属于消息投递时间线的 WhatsApp 事件:它不会出现在 GET /v1/whatsapp/messages/{message_id}/events 中,也无法在那里按条件筛选。
whatsapp_id 指的是被回应的消息,而非回应本身;emoji 是用户所做的变更。用户先回应、再更改表情、最后撤回回应,会在同一条消息上产生三个事件。WhatsApp 在前两次之间不会发送移除,因此更改以携带新表情的单个事件到达。
代码示例
{
  "data": {
    "emoji": "👍",
    "from": { "phone_number": "+14155550100" },
    "to": { "phone_number": "+13124495569" },
    "whatsapp_id": "wam_01ky8b3xq4gd7pmzn2ka51f7te",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b"
  },
  "timestamp": "2026-07-23T14:52:11.000Z",
  "type": "whatsapp.reacted"
}
WhatsApp 以秒为精度报告回应时间,因此上述三个事件中的两个可能共享同一个 timestamp。按该值排序无法确定顺序,按投递顺序也不行,因为重试会使其不可靠。请根据每个事件携带的回应执行操作,将其视为它所描述的变更。不要从事件中重建序列,也不要将最后到达的事件视为消息当前生效的回应,因为时间戳和到达顺序都不支持这样做。回读消息以获取当前生效的回应:GET /v1/whatsapp/messages/{message_id}reactions 中每个发送者返回一个条目,消息的回应日志包含所有变更。
当用户撤回回应时,emoji 存在且为 null,因此 null 表示的是移除本身而非缺失值。表情按 WhatsApp 发送的原样投递,不做规范化,所以 ❤️ 到达时是不同的字符串。

后续步骤