← 所有文章
教程

用 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 作为回复目的地吗?

不要假设二者相同。联系人与会话标识对应不同资源;聊天操作应使用文档规定的会话映射。

LINE 或 Zalo 的姓名也会这样同步吗?

此事件没有文档声明这类支持。统一事件信封不等于各渠道能力一致。

下一步与参考资料

先查看标准事件契约,用受控 WhatsApp 联系人验证合并规则,再启用收件箱更新。

资料核对日期:2026-10-02。

UnifyPort API

让消息接入变成一条稳定的产品管线。

先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。