The migration has two phases and the payload shows both. Meta spent 2026 adding identifiers alongside the phone number, and has now started removing the phone number for users who adopt a username. Here's the same inbound message at all three points.
Phase zero: what you built against
If your integration predates April 2026, this is the shape it expects:
{ "contacts": [ { "profile": { "name": "Sheena Nelson" }, "wa_id": "16505551234" } ], "messages": [ { "from": "16505551234", "id": "wamid.HBgLMTY1MDM4Nzk0MzkV...", "type": "text", "text": { "body": "Does it come in another color?" } } ]}
One identifier, in two places. Lookups, CRM joins and dedupe rules all key on that phone number, because nothing else was available.
Phase one: Meta adds the new identifiers
Since April 2026, the same message arrives like this. Nothing was taken away:
{ "contacts": [ { "profile": { "name": "Sheena Nelson" }, "wa_id": "16505551234", "user_id": "US.13491208655302741918", "parent_user_id": "US.ENT.11815799212886844830" } ], "messages": [ { "from": "16505551234", "from_user_id": "US.13491208655302741918", "from_parent_user_id": "US.ENT.11815799212886844830", "id": "wamid.HBgLMTY1MDM4Nzk0MzkV...", "type": "text", "text": { "body": "Does it come in another color?" } } ]}
This is the payload almost every integration is receiving right now. The four new fields sit alongside the phone number, which is unchanged. No error, no deprecation warning, nothing that required a response. Most integrations don't read them.
The two parent_ fields only show up if you enrol in parent BSUIDs, which Chapter 1.1 covers. The other two arrive for everyone.
Phase two: Meta removes the phone number
Now the same person adopts a username and falls outside the 30-day window:
{ "contacts": [ { "profile": { "name": "Sheena Nelson" }, "profile": { "name": "Sheena Nelson", "username": "realsheenanelson" }, "wa_id": "16505551234", "user_id": "US.13491208655302741918", "parent_user_id": "US.ENT.11815799212886844830" } ], "messages": [ { "from": "16505551234", "from_user_id": "US.13491208655302741918", "from_parent_user_id": "US.ENT.11815799212886844830", "id": "wamid.HBgLMTY1MDM4Nzk0MzkV...", "type": "text", "text": { "body": "Does it come in another color?" } } ]}
wa_id and from are removed: not empty strings, not nulls, just absent from the payload. Any code that reads message["from"] and assumes a value gets nothing. The four identifiers added in phase one are untouched, in the same places under the same keys.
The four identifiers
| Field | What it is | Added | When it appears |
|---|---|---|---|
user_id | The BSUID | April 2026 | Always |
parent_user_id | Parent BSUID | April 2026 | Only for enrolled multi-portfolio businesses |
username | The user's chosen username | With the username rollout | Only if they have adopted one |
wa_id / from | Phone number | Always existed | Only under the conditions below |
Three of the four are new. username tells you this customer's phone number could stop arriving, and a missing wa_id tells you it didn't arrive this time. user_id is the only one you can count on.
When you still get a phone number
Meta doesn't cut you off immediately. It still sends the phone number if any of these hold:
- You messaged or called that phone number in the last 30 days.
- You received a message or call from it in the last 30 days.
- The user is in your contact book, which we cover in Chapter 2.2.
The trap in the 30-day rule
Meta evaluates the 30-day window per business phone number, not per portfolio.
If a customer talks to your support number regularly but has never interacted with your notifications number, webhooks for the notifications number will arrive with no phone number at all, even though another number in the same portfolio talks to them every week.
Outbound status webhooks
Delivery statuses went through the same two phases, under different field names. An entire contacts block appeared on sent, delivered and read statuses, and the statuses block gained its own identifiers:
{ "contacts": [ { "profile": { "name": "Sheena Nelson", "username": "realsheenanelson" }, "wa_id": "16505551234", "user_id": "US.13491208655302741918" } ], "statuses": [ { "id": "wamid.HBgLMTY1MDM4Nzk0MzkV...", "status": "delivered", "recipient_id": "16505551234", "recipient_user_id": "US.13491208655302741918" } ]}
recipient_user_id is always present. recipient_id holds the phone number and can be omitted, and on failed statuses Meta drops the contacts block entirely.
If you reconcile delivery receipts by phone number, that breaks too, for the same reason your inbound handler does.
How these reach you
Everything above is Meta's payload. If you integrate through Kapso you have two ways to receive it, and the choice changes what you see.
| Kapso webhooks | Meta webhooks | |
|---|---|---|
| Payload | Kapso's own format, normalised | Meta's payload, forwarded as-is |
| Identifiers | business_scoped_user_id, parent_business_scoped_user_id and username, on both the contact and the message | The Meta field names shown above |
| Scope | Subscribe to the events you want | Everything for that phone number |
| Buffering | Available | Not available |
The identifiers are in both. You don't need raw Meta forwarding to get BSUIDs, and on Kapso webhooks you don't have to unpack entry[].changes[].value to find them.