用 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 作为回复目的地吗?
不要假设二者相同。联系人与会话标识对应不同资源;聊天操作应使用文档规定的会话映射。
LINE 或 Zalo 的姓名也会这样同步吗?
此事件没有文档声明这类支持。统一事件信封不等于各渠道能力一致。
下一步与参考资料
先查看标准事件契约,用受控 WhatsApp 联系人验证合并规则,再启用收件箱更新。
资料核对日期:2026-10-02。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。