> For the complete documentation index, see [llms.txt](https://docs.channel360.co.za/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.channel360.co.za/api-usage/match-replies-to-your-records/whatsapp-usernames-and-bsuid.md).

# WhatsApp usernames and BSUID

How WhatsApp usernames change identity fields in message:appUser events.

{% hint style="warning" %}
**Draft.** This guidance reflects live traffic captured on 13 August 2026. Provider rollout timing is still being confirmed.
{% endhint %}

## Overview

WhatsApp is separating a user's identity from their phone number by introducing usernames. As this rolls out, identity fields in the `message:appUser` webhook change shape. If your integration reads a phone number from the webhook, update it.

## What changes

The `client` object in the webhook payload is affected. Fields that used to carry a phone number now carry an opaque **Business-Scoped User ID (BSUID)**: a string starting with a two-letter country code, such as `ZA.2c7a9f3e8b...`. A BSUID is not a phone number. When the user still shares their number, it appears in `additionalIdentifiers`.

**Before**

```json
"client": {
  "externalId": "27820000001",
  "displayName": "+27 82 000 0001",
  "raw": {
    "profile": { "name": "Jane Doe" },
    "from": "27820000001"
  }
}
```

**After**

```json
"client": {
  "externalId": "ZA.2c7a9f3e8b...",
  "displayName": "ZA.1000000000000001",
  "additionalIdentifiers": [
    { "key": "phoneNumber", "value": "27820000002" }
  ],
  "raw": {
    "profile": { "name": "Jane Doe" },
    "from": "ZA.2c7a9f3e8b..."
  }
}
```

**`additionalIdentifiers`** is an array of key/value entries, not an object. Find the entry whose `key` is `"phoneNumber"` and use its `value`. Other keys may appear, including `parentUserId`, which identifies a parent account in a portfolio hierarchy. Ignore keys you do not recognise, so your integration keeps working when new ones are added. Live phone values have no leading `+`, for example `"27820000002"`, so normalise them to your own format.

**`displayName`** is no longer a reliable human-readable label: after migration it mirrors the BSUID. Use `raw.profile.name` for the WhatsApp profile name instead. `raw.profile.name` may be absent when the user has not set one.

**`raw.from`** is in transition. It often still carries a phone number today, but it is documented as becoming a BSUID. Do not build on either format; read the phone number from `additionalIdentifiers`.

Users who adopt a WhatsApp username can message you without sharing a number at all. The rest of the payload stays the same, including `trigger`, `appUser`, `conversation` and `messages`.

## What you must do

1. **Stop treating `externalId` and `raw.from` as phone numbers.** `externalId` becomes a BSUID. `raw.from` may contain either format. Channel360 does not rewrite them.
2. **Find the `phoneNumber` entry in `additionalIdentifiers`.** Handle a missing entry. Never store a BSUID in a phone-number field.
3. **Do not assume a cutover date.** Existing contacts are not back-filled, so both formats exist side by side for some time. Detect the format of each value instead.
4. **Match on Channel360 IDs, not the phone number.** Match inbound replies on `conversation._id` and `appUser._id`, and keep your own phone-number record linked to those IDs. See [Match replies to your records](/api-usage/match-replies-to-your-records.md).

## How to detect the format

BSUIDs can contain only digits after the dot, so do not detect them by checking for letters or for numeric-only values.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// A BSUID has an uppercase country prefix and an opaque suffix.
// The suffix may contain only digits.
const isBsuid = (v) =>
  typeof v === 'string' && /^[A-Z]{2}\.[A-Za-z0-9]+$/.test(v);

// Check isBsuid first. A phone contains digits, with an optional '+'.
const isPhone = (v) =>
  typeof v === 'string' && !isBsuid(v) && /^\+?[0-9]{6,15}$/.test(v);

// Find a phone number in the identifier array.
const phoneOf = (client) =>
  (client?.additionalIdentifiers ?? [])
    .filter((i) => i?.key === 'phoneNumber')
    .map((i) => i.value)
    .find(isPhone) ?? null;
```

{% endtab %}

{% tab title="Python" %}

```python
import re

# A BSUID has an uppercase country prefix and an opaque suffix.
# The suffix may contain only digits.
def is_bsuid(v):
    return isinstance(v, str) and re.fullmatch(r"[A-Z]{2}\.[A-Za-z0-9]+", v) is not None

# Check is_bsuid first. A phone contains digits, with an optional '+'.
def is_phone(v):
    return isinstance(v, str) and not is_bsuid(v) and re.fullmatch(r"\+?[0-9]{6,15}", v) is not None

# Find a phone number in the identifier array.
def phone_of(client):
    for i in (client or {}).get("additionalIdentifiers") or []:
        if isinstance(i, dict) and i.get("key") == "phoneNumber" and is_phone(i.get("value")):
            return i["value"]
    return None
```

{% endtab %}
{% endtabs %}

A result of `null` (or `None`) means no phone number is available. This is normal and permanent, so do not retry to obtain one.

## Recommended approach

Treat your own system as the authority for the phone number, and use Channel360 and WhatsApp IDs to match messages. This is the approach in [Match replies to your records](/api-usage/match-replies-to-your-records.md).

## Identity refresh events are not forwarded

Channel360 does not pass `client:update` events to your webhooks, including those with `reason: identifierUpdated`. It does not pass `user:merge` events either, so do not wait for them.

This means the phone number captured when you sent a notification stays authoritative. You are not told when a customer becomes username-only, or when the provider later reveals a phone number. Your record only refreshes when the customer messages you with a phone number present.

To cope with this, keep your own mapping from phone number to `appUser._id` and `conversation._id`, made when you send. [Match replies to your records](/api-usage/match-replies-to-your-records.md) describes where to capture it. That mapping is the source of truth.

## You cannot currently target a BSUID

The Channel360 API accepts phone numbers as send destinations. Sending to a BSUID is not supported.

For a username-only customer, you can [reply](/api-usage/reply-to-a-customer.md) with `POST /org/<ORG_ID>/whatsapp/appuser/<APP_USER_ID>/reply`, within the 24-hour window their message opened. You cannot re-engage them after that window closes, because template sends need a phone number. Capture consent and order details during the live conversation.

## Phone-less customers and subscriber endpoints

Channel360 subscribers are keyed by phone number, so a BSUID-only customer has no subscriber record. They do not appear in subscriber list or detail endpoints, and cannot join recipient lists or campaigns.

Endpoints addressed by phone number return no data for them. You can still [reply](/api-usage/reply-to-a-customer.md) to them by app user ID within the 24-hour window.

## Related

{% content-ref url="/pages/MMPfEfAxW2dosIuGBfym" %}
[Match replies to your records](/api-usage/match-replies-to-your-records.md)
{% endcontent-ref %}

{% content-ref url="/pages/g7KUSUMWQLS6klQCKU15" %}
[Identifiers: which ID is which](/api-usage/concepts/identifiers.md)
{% endcontent-ref %}
