← 所有文章
教程

如何在统一 Webhook 中处理消息表情回应

处理消息表情回应时,应订阅 message.reaction,先用原始请求体验证 Webhook 签名,再把 data.message.target_message_id 作为被回应的原消息 ID。表情位于 data.event.reaction;空字符串表示用户移除了回应。更新业务状态前,还要用顶层事件 ID 去重。

要点速览

  • data.message.id 标识回应本身,不是原消息。
  • data.message.target_message_id 标识收到表情回应的消息。
  • data.event.reaction 保存表情;"" 表示移除。
  • 必须在解析 JSON 前验证 X-Device-Signature
  • 各平台的事件支持不同,不能把有效订阅误认为所有平台都会产生该事件。

读懂 message.reaction 载荷

UnifyPort 将表情回应归一为标准 message.reaction 事件。跨境客服团队可以把 👍 用作已确认标记,把 👎 送入人工复核,也可以只在统一收件箱里同步显示当前表情。这些是业务规则;Webhook 事件本身只描述发生了什么变化。

官方标准 Webhook 事件参考给出的 message.reaction 关键结构如下:

{
  "id": "evt_2f9c1a4b7e",
  "type": "message.reaction",
  "provider": "whatsapp",
  "account_id": "acc_8c21d0",
  "occurred_at": "2026-06-08T12:35:40Z",
  "data": {
    "conversation": { "id": "8613912345678", "type": "user" },
    "sender": { "id": "8613912345678", "type": "user", "name": "Jordan Lee" },
    "message": {
      "id": "wamid.HBgZ",
      "target_message_id": "wamid.HBgM"
    },
    "event": { "kind": "message_reaction", "reaction": "👍" }
  }
}

这里有三个用途不同的 ID。data.message.id 标识回应记录,data.message.target_message_id 指向原消息,顶层 id 则标识这次标准 Webhook 事件。以上示例中,应该把 👍 关联到 wamid.HBgM,而不是 wamid.HBgZ

保存当前状态,而不只是追加事件

事件日志适合审计与排错,但收件箱界面通常需要当前状态。可以用以下字段组合成状态键:provideraccount_iddata.conversation.iddata.message.target_message_iddata.sender.id

data.event.reaction 非空时,保存该发送者对目标消息的当前表情;当它为空字符串时,删除对应状态。不要把空字符串当成一种新表情。

业务投影更新前,应先把顶层事件持久化到可靠队列或数据库,并对顶层 id 建唯一约束。这样,同一次投递发生重试时不会重复执行状态变更。如果现有接收器只订阅了入站消息,可参考 Webhook 事件筛选教程 添加 message.reaction,无需立即改成全量通配订阅。

在 Node.js 中应用回应状态

下面的核心逻辑使用 API 参考中的真实字段。示例用 Map 展示状态投影;生产环境应替换为带事务与唯一约束的数据库表。

const event = JSON.parse(rawBody.toString('utf8'));

if (event.type === 'message.reaction') {
  const { conversation, sender, message, event: detail } = event.data;

  if (!message?.target_message_id || typeof detail?.reaction !== 'string') {
    throw new Error('Invalid message.reaction payload');
  }

  const key = [
    event.provider,
    event.account_id,
    conversation.id,
    message.target_message_id,
    sender.id,
  ].join(':');

  if (detail.reaction === '') reactionState.delete(key);
  else reactionState.set(key, {
    emoji: detail.reaction,
    reactionMessageId: message.id,
    occurredAt: event.occurred_at,
  });
}

这段业务逻辑必须放在签名验证之后。配置了 signing_secret 时,X-Device-Signature<X-Device-Timestamp>.<原始请求体> 的十六进制 HMAC-SHA256。不要先解析再重新序列化 JSON。完整验证流程见 Webhook 投递与签名文档,重试、时间戳与持久化去重则可继续阅读 HMAC 重放防护与幂等教程

选择订阅方式与失败策略

只处理表情回应的消费者可以配置:

{
  "subscribed_events": ["message.reaction"]
}

统一收件箱通常同时订阅 message.receivedmessage.reaction。只有当端点本身就是能够接收全部公开标准事件的通用采集器时,才适合使用 "*"

事件持久化成功后再返回 2xx;签名无效时应拒绝请求;结构异常的回应事件应进入可观测的错误流程,不能静默修改错误的消息。正式采用表情作为审批信号前,请核对 各平台 Webhook 事件差异

UnifyPort 适合放在哪里

UnifyPort 通过非官方接口,把支持平台的事件归一到同一信封结构。应用可以先按 event.type 分流,再在支持 message.reaction 的平台上复用同一套状态逻辑,而不必在业务层散布多套解析器。

归一化不会创造上游平台原本没有提供的能力。如果表情事件承担合规审批或不可缺失的业务记录,应逐个平台核验支持情况;官方平台 API 的事件契约更适合时,应优先采用官方方案。

常见问题

哪个字段是原消息 ID?

使用 data.message.target_message_iddata.message.id 标识的是回应本身。

如何判断用户移除了表情?

检查 data.event.reaction。空字符串表示移除,非空字符串就是当前表情。

可以用 target_message_id 去重吗?

不可以。多人可以回应同一条消息,同一人也可能更换表情。投递去重使用顶层事件 id;当前状态键则应包含目标消息和发送者。

每个平台都会发送 message.reaction 吗?

不会。事件名称有效不等于每个平台都支持。上线前应查看平台事件矩阵。

下一步

打开 创建 Webhook 端点参考,把 message.reaction 加入 subscribed_events,配置 signing_secret,并分别测试新增与移除表情后再接入生产状态。

来源

官方一手来源,核对日期:2026 年 8 月 20 日。