如何在統一 Webhook 處理訊息表情回應
處理訊息表情回應時,應訂閱 message.reaction,先用原始 request body 驗證 Webhook 簽署,再以 data.message.target_message_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 事件。客服團隊可把 👍 視為確認、把 👎 交由同事覆核,亦可只在共享 inbox 顯示目前表情。這些是應用層規則;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。
儲存目前狀態,而非只追加事件
事件紀錄適合審計及除錯,但 inbox 介面一般需要目前狀態。可用 provider、account_id、data.conversation.id、data.message.target_message_id 及 data.sender.id 組成狀態 key。
data.event.reaction 非空時,儲存該發送者對目標訊息的目前表情;如果是空字串,就刪除相應狀態。不要把空字串當成一個新表情。
更新投影前,先將最上層事件寫入可靠 queue 或資料庫,並對最上層 id 設唯一限制,避免合法重試造成重複變更。如果現有接收器只訂閱入站訊息,可參考 Webhook 事件篩選教學 加入 message.reaction,毋須立即改用全量通配設定。
在 Node.js 套用回應狀態
以下核心邏輯使用 API 參考的真實欄位。示例以 Map 說明投影;正式環境應改用有 transaction 及唯一限制的資料庫。
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>.<原始 request body> 的十六進制 HMAC-SHA256。不要先解析 JSON 再重新序列化。完整流程見 Webhook 投遞與簽署驗證,重試、時間戳及持久化去重則可配合 HMAC 重送防護與冪等教學。
設定訂閱及錯誤處理
只處理表情回應的 consumer 可使用:
{
"subscribed_events": ["message.reaction"]
}
共享 inbox 通常同時訂閱 message.received 及 message.reaction。只有能處理全部公開標準事件的通用 collector,才適合使用 "*"。
事件可靠寫入後才回傳 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;目前狀態 key 則包括目標訊息及發送者。
每個平台都會傳送 message.reaction 嗎?
不會。事件名稱有效與平台實際支援是兩回事,上線前請查閱事件矩陣。
下一步
開啟 建立 Webhook 端點參考,把 message.reaction 加入 subscribed_events,設定 signing_secret,並測試新增及移除表情。
來源
官方第一手來源,查核日期:2026 年 8 月 20 日。