使用 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;
}
先驗證所有目標欄位,再套用任何欄位,避免無效資料留下更新一半的狀態。未知欄位不直接複製到檢視;若保留政策允許,可另存已通過簽章驗證的事件,以便日後檢查結構變化。
以欄位層級狀態設計接收器
- 明確訂閱。 新增
contact.updated時,不要移除其他必要事件。事件篩選指南說明了明確清單與萬用字元收集器的差異。保留既有簽章設定。 - 驗證並保存。 依Webhook 傳遞契約,用
signing_secret驗證X-Device-Timestamp、句點及原始請求本文組成的 HMAC-SHA256,並檢查時間戳記是否過期。先持久保存已驗證事件,再回傳 2xx。簽章正確但結構無效的事件應隔離檢查,不要默默套用。 - 解析身分。 依事件類型與 provider 分流。使用工作區、provider、
account_id限定data.contact.id的範圍。只儲存明確提供的conversation_id,不要從電話號碼或聯絡人 ID 組出對話識別碼。缺少對應關係時,不得合併無關記錄。 - 處理重複與順序。 普通事件在工作區內依事件 ID 去重。傳遞順序沒有保證;建議為每個姓名欄位各自記錄最後套用的
occurred_at及同時間排序用的事件 ID,而非僅替整個聯絡人記錄版本。較早事件可能包含較新部分更新未碰觸的欄位。這是應用程式自己的記帳方式,不是新增 API 欄位,也不保證完整還原上游因果順序。 - 明確定義顯示來源。 例如優先顯示本機暱稱,再使用通訊錄全名。欄位清空後,從其他允許的來源重新計算顯示名稱,不要從舊快取恢復剛清除的值。
去重、欄位版本判斷與檢視更新應在同一交易或序列化工作程序中執行。若先標記已處理再寫入資料庫,當機可能造成更新遺失。
驗收情境與限制
測試只更新全名、只更新名字、空字串清空、省略欄位、無效 null、重複傳遞、順序顛倒的部分更新,以及不同訊息帳號中相同聯絡人 ID 的隔離。也要確認本機暱稱與帳號個人資料不變。這些是建議測試,並非已執行的正式環境結果。
UnifyPort 是非官方介面。此事件不是完整通訊錄快照、保證重播機制或刪除聯絡人的訊號。姓名清空不等於聯絡人被刪除。有效訂閱也不保證每次上游異動都會送達;應如實呈現過期或待確認狀態,並在依賴此功能前以已連結帳號驗證行為。
常見問題
未提供 full_name 是否代表姓名被刪除?
不是。保留原值;只有明確提供空字串才會清空該欄位。
可以直接用 contact.id 當作回覆目的地嗎?
不要假設它等於對話 ID。兩者代表不同資源,聊天操作應使用文件定義的對話對應。
LINE 或 Zalo 的聯絡人姓名也能如此同步嗎?
此事件未記載這類支援。統一事件封裝不代表各管道能力相同。
下一步與參考資料
先閱讀標準事件契約,用受控 WhatsApp 聯絡人驗證合併規則,再啟用收件匣更新。
資料核對日期:2026-10-02。
讓訊息接入變成一條穩定的產品管線。
先用統一 API 跑通傳送,再用標準事件把所有入站訊息接回業務系統。