如何在统一 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。
保存当前状态,而不只是追加事件
事件日志适合审计与排错,但收件箱界面通常需要当前状态。可以用以下字段组合成状态键:provider、account_id、data.conversation.id、data.message.target_message_id 和 data.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.received 和 message.reaction。只有当端点本身就是能够接收全部公开标准事件的通用采集器时,才适合使用 "*"。
事件持久化成功后再返回 2xx;签名无效时应拒绝请求;结构异常的回应事件应进入可观测的错误流程,不能静默修改错误的消息。正式采用表情作为审批信号前,请核对 各平台 Webhook 事件差异。
UnifyPort 适合放在哪里
UnifyPort 通过非官方接口,把支持平台的事件归一到同一信封结构。应用可以先按 event.type 分流,再在支持 message.reaction 的平台上复用同一套状态逻辑,而不必在业务层散布多套解析器。
归一化不会创造上游平台原本没有提供的能力。如果表情事件承担合规审批或不可缺失的业务记录,应逐个平台核验支持情况;官方平台 API 的事件契约更适合时,应优先采用官方方案。
常见问题
哪个字段是原消息 ID?
使用 data.message.target_message_id。data.message.id 标识的是回应本身。
如何判断用户移除了表情?
检查 data.event.reaction。空字符串表示移除,非空字符串就是当前表情。
可以用 target_message_id 去重吗?
不可以。多人可以回应同一条消息,同一人也可能更换表情。投递去重使用顶层事件 id;当前状态键则应包含目标消息和发送者。
每个平台都会发送 message.reaction 吗?
不会。事件名称有效不等于每个平台都支持。上线前应查看平台事件矩阵。
下一步
打开 创建 Webhook 端点参考,把 message.reaction 加入 subscribed_events,配置 signing_secret,并分别测试新增与移除表情后再接入生产状态。
来源
官方一手来源,核对日期:2026 年 8 月 20 日。