← 所有文章
教學

用 contact.updated Webhook 同步 WhatsApp 聯絡人姓名

要保持共用收件箱內的 WhatsApp 聯絡人姓名最新,應將 UnifyPort 的 contact.updated 當作通訊錄局部更新,而不是替換整筆聯絡人記錄。套用實際提供的姓名字串,保留未提供的欄位;空字串代表明確清空,null 則不合法。聯絡人資源 ID 與對話 ID 必須分開,也不要覆寫客服設定的本地暱稱或已連接訊息帳戶本身的資料。

重點

  • 此事件的文件範圍是 provider: whatsapp,不包括 whatsapp-protocol,也不代表所有渠道均支援。
  • 欄位缺省與姓名為空,意思不同。
  • 通訊錄姓名、本地別名和訊息帳戶自身名稱應分開儲存。
  • 先驗證簽署及持久保存事件,再更新收件箱檢視。

究竟是哪個名字改了?

WhatsApp 的官方聯絡人管理說明介紹了從已連結裝置管理聯絡人的方向。這解釋了為何姓名可能在客服應用程式之外改動,但它並不定義 UnifyPort 事件,也不保證每次編輯都會發出事件。

UnifyPort 標準事件參考將 contact.updated 定義為 WhatsApp 通訊錄姓名變更。它不是新增聯絡人,也不是將聯絡資料傳送給別人。需要這兩種主動操作時,請看新增聯絡人與傳送 vCard 的分別。

資料意思建議儲存邊界
data.contact.id聯絡人資源 ID配合工作區、provider 及訊息帳戶作為鍵值
data.contact.conversation_id提供時代表相關對話 ID保存明確對應,不從聯絡人 ID 推算
data.contact.address_book本次提供的通訊錄姓名欄位只合併實際提供且支援的欄位
客服本地暱稱應用程式自己的顯示標籤獨立保存,不由此事件覆寫
account.profile.updated已連接訊息帳戶自己的公開名稱資料交由另一個處理器

客戶聯絡人改名,不等於商戶帳戶改名,也不是訊息編輯。

按局部更新處理 payload

以下示例符合文件結構;ID 和姓名均為示範,並非擷取自真實客戶事件:

{
  "id": "0000000000000000000000000000000000000000000000000000000000000191",
  "type": "contact.updated",
  "provider": "whatsapp",
  "account_id": "acc_example",
  "occurred_at": "2026-09-20T03:00:00Z",
  "data": {
    "contact": {
      "id": "15550000002@s.whatsapp.net",
      "conversation_id": "100000000000002@lid",
      "address_book": {
        "full_name": "Example customer",
        "first_name": "Example"
      }
    },
    "event": {
      "kind": "contact_updated",
      "source": "address_book",
      "changed_fields": ["address_book.full_name", "address_book.first_name"],
      "changed_at": "2026-09-20T03:00:00Z"
    }
  }
}

以 address_book 內實際存在的值為準。即使 changed_fields 列出某個欄位,只要物件沒有提供其值,就不要理解為刪除,更不要自動補成 ""。

輸入欄位處理方式
非空字串更新該通訊錄欄位
""清空該欄位
未提供保留原值
null 或其他非字串值拒絕套用更新,交由校驗覆核

以下 JavaScript 只合併文件中的兩個姓名欄位,不是完整的 Webhook 接收器、身份解析器或事件排序實作:

function mergeAddressBook(current, patch) {
  if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
    throw new Error('Invalid address_book object');
  }
  const fields = ['full_name', 'first_name'];
  for (const field of fields) {
    if (Object.hasOwn(patch, field) && typeof patch[field] !== 'string') {
      throw new Error('Invalid address-book name');
    }
  }
  const next = { ...current };
  for (const field of fields) {
    if (Object.hasOwn(patch, field)) next[field] = patch[field];
  }
  return next;
}

先驗證所有目標欄位,再套用任何欄位,避免無效資料留下只更新一半的狀態。未知欄位不會直接複製到檢視;如保留政策容許,可另存已通過簽署驗證的事件,供日後檢查結構變化。

以欄位級狀態設計接收器

  1. 明確訂閱。 加入 contact.updated 時,不要移除其他必需事件。事件篩選指南解釋了明確清單與萬用字元收集器的分別。保留現有簽署設定。
  2. 驗證並保存。 按Webhook 傳遞契約,用 signing_secret 驗證 X-Device-Timestamp、句點和原始請求內容組成的 HMAC-SHA256,並檢查時間戳是否過期。先可靠保存已驗證事件,再回傳 2xx。簽署有效但結構不合法的事件應隔離覆核,不要默默套用。
  3. 解析身份。 按事件類型與 provider 分流。用工作區、provider、account_id 限定 data.contact.id 的範圍。只保存明確提供的 conversation_id;不要用電話號碼或聯絡人 ID 組出對話 ID。缺少對應關係時,不能合併無關記錄。
  4. 處理重複及亂序。 普通事件在工作區內按事件 ID 去重。傳遞順序沒有保證;建議為每個姓名欄位分別記錄最後套用的 occurred_at 和同一時間排序用的事件 ID,而非只記錄整個聯絡人的版本。較早事件可能包含較新局部更新未觸及的欄位。這是應用程式本地記錄規則,不是額外 API 欄位,也不保證完全重建上游因果順序。
  5. 保留顯示來源。 例如先用本地暱稱,再用通訊錄全名。欄位清空後,從其他容許來源重新計算顯示名稱,不要從舊快取恢復剛清掉的值。

去重、欄位版本判斷和檢視更新應放在同一資料庫交易或串行工作程序內。若先標記「已處理」再寫入資料庫,程序中斷可能導致更新遺失。

驗收情境與限制

測試只更新全名、只更新名字、空字串清空、欄位缺省、非法 null、重複傳遞、乱序局部更新,以及不同訊息帳戶出現相同聯絡人 ID 的隔離。也要確認本地暱稱與帳戶資料沒有改動。這些是建議測試,不是已完成的正式環境結果。

UnifyPort 是非官方介面。此事件不是完整通訊錄快照、保證重播機制或刪除聯絡人的訊號。清空姓名不等於刪除聯絡人。有效訂閱亦不保證每次上游變更都會送達;應如實顯示過期或待確認狀態,並在依賴此功能前用已連接帳戶驗證行為。

常見問題

缺少 full_name 是否代表姓名已刪除?

不是。保留原值;只有明確提供空字串,才清空該欄位。

可以直接用 contact.id 作回覆目的地嗎?

不要假設它與對話 ID 相同。兩者代表不同資源,聊天操作應使用文件規定的對話對應。

LINE 或 Zalo 的聯絡人姓名也會同步嗎?

此事件沒有記載這類支援。共用事件格式不代表每個渠道都有相同能力。

下一步與參考資料

先閱讀標準事件契約,用受控 WhatsApp 聯絡人驗證合併規則,再啟用收件箱更新。

資料核對日期:2026-10-02。

UnifyPort API

令訊息接入變成一條穩定嘅產品管線。

先用統一嘅 API 跑通發送,再用標準事件將所有入站訊息接返去業務系統。