← 所有文章
教學

如何在統一 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 介面一般需要目前狀態。可用 provideraccount_iddata.conversation.iddata.message.target_message_iddata.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.receivedmessage.reaction。只有能處理全部公開標準事件的通用 collector,才適合使用 "*"

事件可靠寫入後才回傳 2xx;簽署無效時拒絕請求;結構不完整的回應事件應進入可觀察的錯誤流程。如果表情會用作審批依據,請先查看 各平台 Webhook 事件差異

UnifyPort 適合的位置

UnifyPort 透過非官方接口,把支援平台的事件統一成同一信封結構。應用程式可以先按 event.type 分流,再於支援 message.reaction 的平台共用同一套狀態處理邏輯。

統一格式不會創造上游平台未提供的能力。如果表情事件是合規審批或不可缺少的紀錄,應逐一確認每個平台;官方平台 API 的事件合約更合適時,應使用官方方案。

常見問題

哪個欄位是原訊息 ID?

使用 data.message.target_message_iddata.message.id 代表回應本身。

如何判斷表情已被移除?

檢查 data.event.reaction。空字串表示移除;非空字串就是目前表情。

可以用 target_message_id 去重嗎?

不可以。多人可以回應同一則訊息,同一人亦可更換表情。投遞去重使用最上層事件 id;目前狀態 key 則包括目標訊息及發送者。

每個平台都會傳送 message.reaction 嗎?

不會。事件名稱有效與平台實際支援是兩回事,上線前請查閱事件矩陣。

下一步

開啟 建立 Webhook 端點參考,把 message.reaction 加入 subscribed_events,設定 signing_secret,並測試新增及移除表情。

來源

官方第一手來源,查核日期:2026 年 8 月 20 日。