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