← 所有文章
教學

如何在 WhatsApp 共用收件匣同步已讀與未讀狀態

WhatsApp 共用收件匣不應在 Webhook 一到就把對話標為已讀,而應等團隊真正接手或完成處理後再更新。透過 UnifyPort,可以用 conversation_id 呼叫對話已讀端點;若要把 WhatsApp 回條精確推進至某則訊息,還必須同時提交訊息 ID 與傳送者 ID。需要再次處理時,則將對話標為未讀。

重點整理

  • 對話已讀與未讀動作目前只支援 WhatsApp;不支援該動作的渠道組合會回傳 501 unsupported_by_provider
  • POST /v1/accounts/{account_id}/conversations/read 必須接收 conversation_id,也能選擇推進到特定訊息。
  • up_to_message_idup_to_message_sender_id 必須成對出現;只傳其中一個會回傳 400 invalid_request
  • 標記未讀只需要 conversation_id
  • 指派、追蹤與結案狀態仍應保存在自己的系統。渠道端已讀狀態只是共用收件匣的投影,不是完整工單資料庫。

先分清三種「已讀」

可靠的共用收件匣必須分開處理以下概念:

  1. 團隊佇列狀態:例如新訊息、已指派、等待中、已解決,由你的應用程式定義。
  2. 已連接 WhatsApp 帳號的聊天清單狀態:已讀或未讀。透過標記對話已讀端點標記對話未讀端點更新。
  3. 收件者回條message.read Webhook 表示收件者讀過帳號送出的某一則或多則訊息,不等於客服開啟了入站工單。

本機對話設定變更時,UnifyPort 也可以映射 conversation.updated 事件。data.conversation.id 識別聊天,已讀狀態變更可出現在 data.read。請把它當成對帳訊號;誰接手、何時解決、為何重新開啟,仍要寫入自己的資料庫。

套用任何事件前,應先驗證原始請求內容的簽章,並讓重試具備冪等性。接收端邊界可參考Webhook HMAC、重播防護與重試指南。如果端點只需要收件匣事件,也可依照Webhook 事件篩選教學明確訂閱所需事件。

決定何時更新渠道狀態

不要在每個入站 Webhook 抵達時自動標為已讀,否則無人處理的佇列看起來也像已清空。建議採用清楚的規則:

團隊動作本機佇列狀態WhatsApp 動作
入站訊息已儲存new不操作
客服接手對話assigned視需要推進到已接手的訊息
客服完成對話resolved將整段對話標為已讀
客服要求後續追蹤waiting將對話標為未讀
自動化在指派前失敗new不操作

這種分離也可避免瀏覽器重新整理、Webhook 重試或背景預覽意外清掉待辦。

將 WhatsApp 對話標為已讀

conversation_id 放在 JSON 請求內容中,而不是 URL 路徑,因為渠道識別碼可能含有 @: 等字元。

將整段對話標為已讀:

curl -X POST "https://api.unifyport.ai/v1/accounts/$UNIFYPORT_ACCOUNT_ID/conversations/read" \
  -H "X-Api-Key: $UNIFYPORT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "8613912345678@s.whatsapp.net"
  }'

若要把 WhatsApp 回條推進到明確的入站訊息,請從同一個 message.received 事件複製三個識別碼:

{
  "conversation_id": "120363041234567890@g.us",
  "up_to_message_id": "CURRENT-MESSAGE-ID",
  "up_to_message_sender_id": "8613912345678@lid"
}

群組訊息中的 up_to_message_sender_id 必須使用相符的 data.sender.id,不可從對話 ID 推算。不需要訊息層級回條時,請同時省略兩個 up_to_message_* 欄位。

以下 Node.js helper 會先確認欄位成對,再送出請求:

const apiBase = 'https://api.unifyport.ai/v1';

async function setWhatsAppReadState({ accountId, conversationId, unread, message }) {
  const action = unread ? 'unread' : 'read';
  const body = { conversation_id: conversationId };

  if (!unread && message) {
    if (!message.id || !message.senderId) {
      throw new Error('message.id and message.senderId must be supplied together');
    }
    body.up_to_message_id = message.id;
    body.up_to_message_sender_id = message.senderId;
  }

  const response = await fetch(
    `${apiBase}/accounts/${encodeURIComponent(accountId)}/conversations/${action}`,
    {
      method: 'POST',
      headers: {
        'X-Api-Key': process.env.UNIFYPORT_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    }
  );

  if (!response.ok) {
    const failure = await response.json();
    throw new Error(`${response.status} ${failure.error?.code ?? 'unknown_error'}`);
  }

  return response.json();
}

在已完成簽章驗證的事件處理器中,直接使用文件定義的欄位:

await setWhatsAppReadState({
  accountId: event.account_id,
  conversationId: event.data.conversation.id,
  unread: false,
  message: {
    id: event.data.message.id,
    senderId: event.data.sender.id
  }
});

將待追蹤對話標為未讀

未讀動作較簡單:

await setWhatsAppReadState({
  accountId: event.account_id,
  conversationId: event.data.conversation.id,
  unread: true
});

只有在團隊明確重新開啟工作時才使用。渠道端未讀狀態不能取代自己的提醒機制;追蹤負責人、到期狀態與原因都應保存在本機佇列。

對帳時避免形成循環

應用程式呼叫對話動作後,Webhook 可能收到相應的 conversation.updated 事件。替本機發起的操作保存內部操作紀錄,讓後續事件用來確認狀態,而不是再次觸發相同動作。

安全流程如下:

  1. 以冪等方式儲存 message.received
  2. 在交易中更新本機工單狀態。
  3. 呼叫渠道狀態動作。
  4. API 回傳 { "data": { "ok": true } } 後才記錄成功。
  5. conversation.updated 視為確認,或視為來自已連接帳號的外部變更。
  6. 狀態不明時,使用取得指定對話端點讀取該對話,再比較 unread_count

替其他渠道啟用控制項前,請查看目前的各渠道統一動作支援矩陣。統一路由不代表每個渠道都實作每一種動作。

限制與取捨

如果流程依賴渠道認證的企業功能、官方範本訊息或原生治理,官方方案可能更合適。UnifyPort 的非官方介面適用於一般帳號訊息流程,但不會讓所有渠道具備完全相同的功能。

就本文流程而言,對話已讀與未讀目前只支援 WhatsApp。其他渠道的共用收件匣應隱藏或停用這些控制項,不要把 501 unsupported_by_provider 當成可持續重試的暫時錯誤。

常見問題

收到 message.received 會自動將聊天標為已讀嗎?

不會。接收與儲存事件不應清空待辦;只有到達團隊定義的工作流程節點時,才呼叫已讀端點。

message.read 與標記對話已讀有何不同?

message.read 是收件者閱讀訊息後的回條事件;對話已讀動作改變的是已連接帳號本機聊天清單狀態。

可以只傳 up_to_message_id 嗎?

不可以。必須連同 up_to_message_sender_id 一起傳送;或兩者都省略,將整段對話標為已讀。

Telegram、LINE、TikTok、Zalo 與 X 能使用相同控制嗎?

目前不能。支援矩陣將對話已讀與未讀列為 WhatsApp 專屬動作;不支援的組合會回傳 501 unsupported_by_provider

下一步

先閱讀標記對話已讀 API Reference,並在定義本機重新開啟規則後,再加入未讀動作。

第一手來源

截至 2026 年 8 月 21 日核對: