电话号码已经不再是 WhatsApp 识别你客户的方式了。大多数 CRM 还没跟上。
WhatsApp 正在把客户身份从电话号码上迁走,而如果你在运营一个 CRM 或任何一种消息集成,这个变更已经落到你头上了。自 2026 年 3 月底到 4 月初的这次推出以来,每一条 WhatsApp Cloud API 的 message webhook 都携带了一个叫 Business-Scoped User ID(简称 BSUID)的新身份字段,那是 Meta 按 business portfolio 签发的一个不透明 token。[1][2] 它作为自己的字段出现,而不是塞进旧的电话字段里的一个新值,并且一旦用户挑了一个 WhatsApp username,电话号码本身现在就可能缺失。如果读完这篇文章你只改一件事,就改这件:在每一条入站消息上把 BSUID 当作身份主键,把电话号码保留为一个可空的别名,并且不要再假设 to、from 或 wa_id 会装着电话号码。[2] 本文剩下的部分就是那份逐字段的对照表,对照过主文档核验过,外加那些已经在流传的错误做法。
有两件事正在同时落地,而几乎所有人都把它们混着跑。一件是面向用户的隐私功能,也就是 WhatsApp username,它让某个人不必交出号码就能联系到一家业务。另一件是面向开发者的身份变更,也就是 BSUID,无论某个用户是否设置过 username,它都会出现在 Cloud API 里。username 抢了头条,但 BSUID 才是那个悄悄弄坏集成的家伙,所以把这两件事分开看是值得的。它们跑在不同的时间线上,触碰你技术栈里不同的部分。
两层,不是一层
大多数报道把「WhatsApp usernames」和「BSUID」当作一次发布。它们其实是两层,瞄准不同的人,影响半径也大相径庭。
| 层 |
它是什么 |
影响谁 |
截至 2026 年 6 月的状态 |
| WhatsApp username |
一个可选的公开 handle,让用户不必分享电话号码就能被联系到 [2][6] |
终端用户(opt-in,没有人被强制采用) |
自 2026 年 4 月起限量 beta,更广泛的推出从 2026 年 6 月开始 [2][3][5] |
| Business-Scoped User ID(BSUID) |
Cloud API 里一个新的不透明身份 token,对每一个用户都存在,无论是否采用 username [1][2] |
平台上的每一家业务、每一个 CRM 和每一个 BSP 集成 |
自 2026 年 3 月底到 4 月初的推出以来,已在 message webhooks 中上线 [1][2][5] |
正是那个时间差把团队绊倒了。你不能等 username 推完了再去做这项工作,因为开发者那一侧已经发货了。BSUID 现在就在你的 webhooks 里,包括那些永远不会去碰 username 功能的用户。
推出节奏,带日期
下面这些标注了日期的里程碑,取自那些镜像了 Meta 合作方指引的 BSP 文档。凡是某个日期是「正在推进中」而不是一个固定切换点的,我们都会说明。
| 日期 |
里程碑 |
层 |
| 2026 年 3 月底 / 4 月初 |
BSUID 开始出现在 Cloud API 和 BSP 的 message webhooks 里。Meta 的主页面写的是 4 月初;Azure 和 360dialog 的时间线表写的是 3 月 31 日 [1][2][5] |
开发者 |
| 2026 年 4 月初 |
Contact Book 上线:一个由 Meta 托管、默认开启的 phone-to-BSUID 映射存储 [2] |
开发者 |
| 2026 年 4 月 |
WhatsApp username 对一小撮用户限量 beta [3][5] |
用户 |
| 2026 年 5 月(确切日期待定) |
Meta 的直连 Cloud API 开始通过新的 recipient 字段支持发送到一个 BSUID [1] |
开发者 |
| 2026 年 6 月 |
username 开始触达终端用户,业务可以认领 handle;Azure 和 Twilio 把它们包装层的发送可用性框定在这附近 [2][3] |
用户 |
| 2026 年余下时间 |
分阶段、一国接一国地向广泛可用扩展 [5] |
用户 |
有一个日期值得加一句注解,而它正好干净地演示了本文反复回到的那个「原始 API 对比 BSP」的分裂。Meta 自己的 BSUID 页面说,直连 Cloud API 要到 2026 年 5 月才会支持发送到一个 BSUID,确切日期待定。[1] Azure 和 Twilio 把生产可用性框定在 2026 年 6 月前后,绑定到 username 触达终端用户。[2][3] 所以把外发可用性当作因供应商而异的事:原始契约是新的 recipient 字段,但你实际什么时候能用上它,取决于你是在直连 API 上还是在某个 BSP 上,而两者各自可以在自己的日期落地。和你的供应商确认你的情况。
入站 webhook 实际长什么样
这正是大多数文章写错的地方。在直连 Cloud API 上,BSUID 是它自己的字段。它不是旧的 wa_id 携带了一种新值。
| 字段(原始 Cloud API) |
携带 |
何时存在 |
contacts[].user_id |
用户的 BSUID |
总是,在每一条 message webhook 里 [1][6] |
messages[].from_user_id |
发送者的 BSUID |
总是,在每一条 message webhook 里 [1] |
contacts[].profile.username |
WhatsApp username |
仅当用户设置了一个时 [1][6] |
contacts[].wa_id |
电话号码 |
当电话号码可用时;一旦采用了 username 且电话号码包含条件不满足就会被省略 [2] |
messages[].from |
发送者的电话号码 |
与 wa_id 同样的条件 [2] |
statuses[].recipient_user_id |
收件人的 BSUID |
在 status webhooks 上,无论原始发送用的是电话号码还是 BSUID。唯一的省略情况:一条原本以电话号码寻址的消息所对应的失败状态 [1][6] |
要记住的形态是:user_id 和 from_user_id 总是在场,而电话字段不一定。当电话号码可用时,你会一次拿到两者,电话号码在 wa_id 和 from 里,BSUID 在 user_id 和 from_user_id 里。[2] 一旦某个用户采用了 username 并且和你没有近期往来历史,电话字段就可能空着或缺失过来,而 BSUID 是唯一能标识发送者的东西。
只要下列条件中至少有一个成立,Meta 就会让电话号码继续流过来,这是按 business 的电话号码而不是按 portfolio 来核验的:[2]
- 你在过去 30 天内给该用户的号码发过消息或打过电话。
- 你在过去 30 天内收到过来自该用户号码的消息或来电。
- 该用户在你的 Contact Book 里。
所以一段进行中的对话没问题。号码缺失发生在那些全新的、只有 username 的、没有历史的联系人身上,而这恰好就是一家做 lead-gen 的业务整天都在打交道的那一类人:一个你从没说过话的人发来的第一条消息。
直连 Cloud API 的名字和你 BSP 的名字不一样
如果你不在直连 Cloud API 上,你的 Business Solution Provider 已经把这些字段重命名了。读一份 BSP 集成指南,然后跑到一个原始 Cloud API 负载里去找那些名字,是一条常见的死路。
| 表面 |
BSUID 字段名 |
来源 |
| 直连 Cloud API |
contacts[].user_id 和 messages[].from_user_id |
[1] |
| Twilio |
ExternalUserId |
[3] |
| Infobip |
contact.userId |
[4] |
| Azure Communication Services |
fromBSUID(入站)和 toBSUID(投递状态) |
[2] |
底下不管哪种方式都是同一个不透明的 BSUID。变的只是包在它外面的那一层。
外发寻址:端点不变,但原始 API 增加了 recipient
发送端点不变,但原始 Cloud API 的字段变了。电话号码发送仍然用 to。BSUID 发送用一个新的 recipient 字段。[1] 两者至少要有一个在场,如果你两者都发,Meta 让 to 优先,所以它们是两个独立的寻址输入,而不是一个会自动判别你递进去什么的单一字段。[1] 这正是 BSP 包装层和直连契约分歧最大的地方:比如 Azure Communication Services 在它那一个 to 数组里既收电话号码也收 BSUID,并替你分拣好 [2],这是个方便的归一化处理,但不是原始 API 的做法。如果你的文章自称在描绘直连 Cloud API(就像这篇这样),那 recipient 才是要紧的字段。
格式两边都很挑剔。一个 BSUID 是一个 ISO 3166 alpha-2 country code,然后一个句点,然后最多 128 个字母数字字符,像 US.13491208655302741918。country code 和句点是值的一部分 [2],所以剥掉它们、或者像你整理电话号码那样去「归一化」BSUID,都会弄坏发送。
有一个真正的例外。Authentication 类别的模板仍然需要电话号码。One-tap、zero-tap 和 copy-code 这些 auth 模板不能发往一个 BSUID,尝试这样做会返回 Meta 错误码 131062。[6] 如果你的一次性密码流程跑在 WhatsApp 上,它就需要号码,这又多了一个理由,让你趁还有电话号码时就抓住 phone-to-BSUID 这一对。
身份模型:不透明、按作用域划分,且并不总能逆转
BSUID 的行为方式,决定了它在你数据模型里该放在哪儿。下面每一行都对应一个设计决策。
| 属性 |
行为 |
| 作用域 |
每个 business portfolio 唯一。同一个用户和每一家业务交互时拿到的 BSUID 都不一样,所以它不是一个全局的人物主键。[1][2] |
| 不透明性 |
一个不透明 token。它不是电话号码,除了 country 前缀之外不携带任何可解析的含义。[2] |
| 格式 |
{ISO 3166 alpha-2}.{最多 128 个字母数字},整体使用,包括 country code 和句点。[2] |
| username 变更 |
稳定。更改 username 不会改变 BSUID。[2] |
| 电话号码变更 |
重新生成。更改电话号码会产生一个新的 BSUID,并且 Meta 会发一条 user_id_update webhook,同时携带旧的和新的 BSUID,好让你重新挂接。[1][2] |
标识符变更时重新挂接,不要重复创建
当某人更改了他账户上的电话号码,BSUID 会被重新生成,而 Meta 通过一条已记录在案的 user_id_update webhook 来宣告这件事,该 webhook 同时携带旧的和新的 BSUID。[1][2] 用旧的 BSUID 去找到那条已有记录,用新的去更新它,这样这次变更就落成一次合并,而不是一个新的联系人。把它当成一个全新的用户来处理,你就会让一个人的历史被分裂在两条记录里。
有一点要弄对:这是它自己的标识符变更负载,不是一个投递状态,所以不要把重新挂接接到本对照表别处的 statuses[] 回调上。有些 BSP 文档还会为同一次变更暴露一条并行的 system message,所以在你拿它来开发之前,对着 Meta 的在线参考确认确切的负载形态。各处的行为是一样的:标识符变了,所以重新挂接,而不是重复创建。
电话号码恢复是只向前的
Meta 的 Contact Book 会自动存储 phone-to-BSUID 的映射。它由 Meta 托管、默认开启,并且作用域限定在 business portfolio。[2] 让人意外的那个坑是:它只记录它在 2026 年 4 月上线之后的交互,永远不会回填,对于一个从未分享过号码、全新的、只有 username 的联系人,它根本没什么可记录。[2] 所以你不能指望以后还能为一个只有 BSUID 的联系人恢复出电话号码。
这就引出一个简单的习惯:每当一条 webhook 确实包含电话号码时,就在那一刻存下 phone-to-BSUID 这一对。不要留到以后。
那个标准数据模型范式
每一个 CRM 和消息集成最后都会走到同一个形态,而它是一个建立在一条规则之上的小改动。BSUID 是主键;电话号码是别名。
- 把 BSUID 存为每个 (business, contact) 的持久身份主键。把电话号码保留为同一条记录上一个补充性的、可空的别名。[2][5]
- 在每一条入站消息上读取 BSUID 字段(
user_id 或 from_user_id,或你 BSP 的等价物)作为可靠的身份。不要再把 to、from 或 wa_id 当作保证存在的电话号码来读。[2]
- 只要电话号码在场就捕获 phone-to-BSUID 这一对,因为 Contact Book 不会替你回填它。[2]
- 在双标识符上合并。如果你已经按电话号码有了一个联系人,而在一段活跃对话期间同一个人又来了一个新的 BSUID,把它们关联起来,而不是创建第二条记录。
- 在标识符变更事件上,把已有的联系人重新挂到新的 BSUID 上。[2]
如果你的 schema 把 WhatsApp 联系人键在电话号码上,那就是第一件要修的事,因为电话号码现在是那个可能消失的字段,而 BSUID 才是那个总是到达的字段。
常见误解
这些在厂商博文和集成讨论串里已经在流传了。大多数都是那种听上去合理、能顺利通过 review、然后在第一个只有 username 的线索出现时就翻车的捷径。
BSUID 不过就是 wa_id 字段换了一种新值。 不是这样。它是一个独立的字段,contacts[].user_id 和 messages[].from_user_id,和电话字段并排坐着。[1] 那些盯着 wa_id 等「一个看起来不一样的值」的代码,在任何同时也有电话号码在场的负载上,会彻底错过 BSUID。
电话号码总是在那儿,所以把 from 当电话号码来解析是安全的。 不再是了。一旦某个用户采用了 username 并且和你没有近期历史,from 和 wa_id 就可能为空或不存在。[2] 任何把那些字段校验或格式化成 E.164 号码的逻辑,会恰好在你最想要的那些联系人,也就是新来的那些,上面崩掉。
你只要把 BSUID 丢进 to 字段,API 就会替你搞定。 那在某些 BSP 包装层上是对的,比如 Azure ACS [2],但在原始 Cloud API 上不是。直连 API 保留 to 给电话号码,并为 BSUID 发送增加一个独立的 recipient 字段,如果两者都在场则 to 优先。[1] 对着原始 API 把一个 BSUID 放进 to,你发出去的就不是你以为的那条。Authentication 模板是又一个例外:它们仍然需要电话号码,如果你把它们指向一个 BSUID 会返回错误码 131062。[6]
你可以剥掉 country 前缀和句点来「清理」BSUID。 你不能。country code 和句点是值的一部分,改动它的任何一部分都会让请求失败。[2]
BSUID 跨平台标识一个人。 它不会。它按 portfolio 划分作用域,所以同一个人在每一家业务那里都带着一个不同的 BSUID。你不能用它在两家业务之间认出某个人,也不能在不相关的 portfolio 之间共享身份。[1][2] 它是你世界内部的身份,不是跨平台的身份。
有了 Contact Book,就意味着你以后总能查到一个电话号码。 它只向前。它记录的是它在 2026 年 4 月上线之后的映射,永远不会回填,对于一个从未分享过号码、只有 username 的联系人,它根本没什么可记录。[2] 趁你还握着的时候,自己把这一对捕获下来。
一个电话号码变更事件意味着一个新用户。 那是同一个用户,BSUID 被重新生成了,所以重新挂接那条已有记录。[2] 反过来把它重复创建,你就会把一个人的历史分裂在两个联系人之间。
给在非官方协议上工作的团队的一点说明:BSUID 不是 @lid
如果你的技术栈也通过 Baileys 这类库碰到了非官方的多设备协议,你大概已经撞上过一个不同的标识符:当一个用户真实的电话号码 JID 被隐藏时出现的 @lid「LinkedID」JID。[8] 这些看起来是一回事,但它们不是。
BSUID 是一个官方的 Cloud API 构造,按 portfolio 划分作用域、不透明,在已记录在案的 webhook 字段里投递。@lid JID 活在 WhatsApp 的多设备寻址内部,是按用户全局的,不限定在某一家业务里。[8] 两者都在追同一个想法,把身份从电话号码上挪走,但它们是不同表面上的不同机制,你为其中一个建的映射不会平移到另一个上。
这个截止时间对 WhatsApp 的约束力,比对其他渠道更强
在 WhatsApp 上,这次身份变更落在一个本就在跟时钟赛跑的渠道之上。Meta 的 Business Platform 给你一个 24 小时客服窗:一旦用户给你发了消息,你有 24 小时可以用自由格式回复,之后你唯一的外发选项就是一条预先审批过的、付费的模板消息。[7] 一个只有 username 的线索发来的第一条消息,可能带着一个 BSUID、没有电话号码、而那个 24 小时计时器已经在倒数了。一个没法用 BSUID 认出、存下并回复这个联系人的集成,缺的不只是一个干净的数据模型。它可能错过那个免费回复窗,连同那条线索一起错过。
所以我们没有把 BSUID 就绪当作可选项,也没有等推出逼我们出手。StaffOS 已经在跑本文所主张的那个范式。我们把 WhatsApp 联系人键在那个持久的平台标识符上,把电话号码保留为一个可空的别名,只要电话号码在场就捕获 phone-to-BSUID 这一对,并在 user_id_update 事件上重新挂接而不是重复创建。对我们来说,一次只有 username 的首触从第一条消息起就是一个普通联系人,而不是一个要靠电话号码逻辑撑着的特殊情况。如果你正在两家 WhatsApp 厂商之间掂量,就把上面那份审计当作及格线:这项工作本该已经做完,而不是还躺在路线图上。
发货之前该核验什么
平台文档是真理之源,而随着面向用户的推出还在继续,它们也仍在被填补。在你针对某个具体负载写代码之前,对着 Meta 的在线参考核验三件事:user_id_update 标识符变更负载的确切形态、你的 BSP 递给你的是原始字段还是它自己重命名过的版本、以及自本文写就以来 contacts 对象里是否出现了任何新字段。上面那份对照表反映的是截至 2026 年 6 月的主文档和合作方文档,等 Meta 改了这个表面,我们会更新它。
如果这里有什么和在线文档对不上了,告诉我们,我们会修。整件事的关键就是它要一直保持可核验。