← 所有文章
教學

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

要更新共用收件匣中的 WhatsApp 聯絡人姓名,應將 UnifyPort 的 contact.updated 視為通訊錄的部分更新,而非替換整筆聯絡人資料。套用實際提供的姓名字串,保留省略的欄位;空字串表示明確清空,null 則不合法。聯絡人資源與對話的識別碼必須分開,也不要覆寫客服設定的本機暱稱或已連結訊息帳號本身的個人資料。

重點整理

  • 此事件的文件範圍是 provider: whatsapp,不包括 whatsapp-protocol,也不代表每個管道皆支援。
  • 姓名欄位省略與姓名為空,意義不同。
  • 通訊錄姓名、本機別名和訊息帳號本身的名稱應分開儲存。
  • 先驗證簽章並持久保存事件,再更新收件匣檢視。

哪一個名字變了?

WhatsApp 的官方聯絡人管理說明介紹了從連結裝置管理聯絡人的方向。這提供了姓名可能在客服應用程式外變更的背景,但它不定義 UnifyPort 事件,也不保證每次編輯都會發出事件。

UnifyPort 標準事件參考將 contact.updated 定義為 WhatsApp 通訊錄姓名異動。這不同於新增聯絡人,也不同於把聯絡資料傳給別人。需要這些主動操作時,請參考新增聯絡人與傳送 vCard 的差異。

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

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

將酬載視為部分更新

以下是符合文件結構的示例;識別碼與姓名皆為示範資料,不是真實客戶事件:

{
  "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 組出對話識別碼。缺少對應關係時,不得合併無關記錄。
  4. 處理重複與順序。 普通事件在工作區內依事件 ID 去重。傳遞順序沒有保證;建議為每個姓名欄位各自記錄最後套用的 occurred_at 及同時間排序用的事件 ID,而非僅替整個聯絡人記錄版本。較早事件可能包含較新部分更新未碰觸的欄位。這是應用程式自己的記帳方式,不是新增 API 欄位,也不保證完整還原上游因果順序。
  5. 明確定義顯示來源。 例如優先顯示本機暱稱,再使用通訊錄全名。欄位清空後,從其他允許的來源重新計算顯示名稱,不要從舊快取恢復剛清除的值。

去重、欄位版本判斷與檢視更新應在同一交易或序列化工作程序中執行。若先標記已處理再寫入資料庫,當機可能造成更新遺失。

驗收情境與限制

測試只更新全名、只更新名字、空字串清空、省略欄位、無效 null、重複傳遞、順序顛倒的部分更新,以及不同訊息帳號中相同聯絡人 ID 的隔離。也要確認本機暱稱與帳號個人資料不變。這些是建議測試,並非已執行的正式環境結果。

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

常見問題

未提供 full_name 是否代表姓名被刪除?

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

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

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

LINE 或 Zalo 的聯絡人姓名也能如此同步嗎?

此事件未記載這類支援。統一事件封裝不代表各管道能力相同。

下一步與參考資料

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

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

UnifyPort API

讓訊息接入變成一條穩定的產品管線。

先用統一 API 跑通傳送,再用標準事件把所有入站訊息接回業務系統。