> ## Documentation Index
> Fetch the complete documentation index at: https://developer.zapsterapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to send messages to a BSUID (WhatsApp official)

> Send messages to a BSUID (Business-Scoped User ID) through the official (WABA) API when the user's phone number is hidden. Examples in cURL, Node.js, Python, and Go.

This guide shows how to send messages to a **BSUID** (Business-Scoped User ID), the phone-less identifier Meta uses on official WhatsApp (WABA). The send uses the same `POST /v1/wa/messages` endpoint, changing only the value of the `recipient` field.

## What a BSUID is

BSUID stands for **Business-Scoped User ID**. It is an identifier Meta generates to represent a WhatsApp user without exposing their phone number.

It exists because of WhatsApp's **username** rollout, planned for 2026. When a user chooses to chat by username, or opts to keep their number hidden, your business stops receiving the phone number and receives a BSUID instead. This identifier lets you continue the conversation normally, except the real phone number is never revealed.

A BSUID is **scoped to a business portfolio**. The same user shows up with a different BSUID for each portfolio, so a BSUID is only valid inside the portfolio that received it (see [Limitations and errors](#limitations-and-errors)).

<Note>
  The BSUID is **exclusive to the official channel (WABA)**. On the unofficial channel (QR code), the analogous identifier is the **LID**, which flows through the normal JID path. A BSUID sent to an unofficial instance is rejected with the `waba_bsuid_requires_waba` error.
</Note>

## Format

A BSUID has two forms:

| Type                                              | Format                              | Example                       |
| ------------------------------------------------- | ----------------------------------- | ----------------------------- |
| **Standard**                                      | country code + `.` + identifier     | `US.13491208655302741918`     |
| **Parent** (businesses managed across portfolios) | country code + `.ENT.` + identifier | `US.ENT.11815799212886844830` |

Format rules:

* The prefix is the **ISO 3166 alpha-2 country code** (two letters, such as `US`, `BR`, `PT`).
* Then comes a dot (`.`).
* **Parent** BSUIDs insert the `ENT` segment (also followed by a dot) between the country code and the identifier.
* The final identifier is up to 128 alphanumeric characters and is **opaque**: treat it as a meaningless string and never alter it.

## How to send

Sending is identical to a regular WABA message. You use the same `POST /v1/wa/messages` endpoint and the same request signature. The only difference is that the `recipient` field takes the BSUID instead of a phone number. Zapster detects the BSUID format automatically and builds the Meta Cloud API payload with the correct field.

Before you start, you need a [connected WABA instance](/en/v1/guides/connect-waba-instance). In every example, replace `YOUR_API_TOKEN` with your API token and `YOUR_INSTANCE_ID` with the WABA instance ID. The example BSUID (`US.13491208655302741918`) stands for the recipient; use the real BSUID that arrived in your webhook.

<Note>
  As with any free WABA message, text, media, and buttons are only delivered inside the **24-hour conversation window**. Outside the window, use an approved **template** (see further below). The full breakdown is in [Sending WhatsApp messages with WABA](/en/v1/guides/send-waba-messages).
</Note>

### Plain text

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zapsterapi.com/v1/wa/messages \
    -H "Authorization: Bearer YOUR_API_TOKEN" \
    -H "X-Instance-ID: YOUR_INSTANCE_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "recipient": "US.13491208655302741918",
      "text": "Hi! Your order is confirmed."
    }'
  ```

  ```javascript Node.js (axios) theme={null}
  const axios = require('axios')

  const response = await axios.post(
    'https://api.zapsterapi.com/v1/wa/messages',
    {
      recipient: 'US.13491208655302741918',
      text: 'Hi! Your order is confirmed.',
    },
    {
      headers: {
        Authorization: 'Bearer YOUR_API_TOKEN',
        'X-Instance-ID': 'YOUR_INSTANCE_ID',
      },
    },
  )

  console.log(response.data.message_id)
  ```

  ```javascript Node.js (fetch) theme={null}
  const response = await fetch('https://api.zapsterapi.com/v1/wa/messages', {
    body: JSON.stringify({
      recipient: 'US.13491208655302741918',
      text: 'Hi! Your order is confirmed.',
    }),
    headers: {
      Authorization: 'Bearer YOUR_API_TOKEN',
      'Content-Type': 'application/json',
      'X-Instance-ID': 'YOUR_INSTANCE_ID',
    },
    method: 'POST',
  })

  const data = await response.json()
  console.log(data.message_id)
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.zapsterapi.com/v1/wa/messages",
      headers={
          "Authorization": "Bearer YOUR_API_TOKEN",
          "X-Instance-ID": "YOUR_INSTANCE_ID",
      },
      json={
          "recipient": "US.13491208655302741918",
          "text": "Hi! Your order is confirmed.",
      },
  )

  print(response.json()["message_id"])
  ```

  ```go Go theme={null}
  package main

  import (
  	"fmt"
  	"io"
  	"net/http"
  	"strings"
  )

  func main() {
  	body := strings.NewReader(`{
  		"recipient": "US.13491208655302741918",
  		"text": "Hi! Your order is confirmed."
  	}`)

  	req, _ := http.NewRequest("POST", "https://api.zapsterapi.com/v1/wa/messages", body)
  	req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN")
  	req.Header.Set("X-Instance-ID", "YOUR_INSTANCE_ID")
  	req.Header.Set("Content-Type", "application/json")

  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer res.Body.Close()

  	data, _ := io.ReadAll(res.Body)
  	fmt.Println(string(data))
  }
  ```
</CodeGroup>

### Media (image, video, or document)

Send the media by public URL or base64 in the `media` field. The type is detected automatically from the file.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zapsterapi.com/v1/wa/messages \
    -H "Authorization: Bearer YOUR_API_TOKEN" \
    -H "X-Instance-ID: YOUR_INSTANCE_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "recipient": "US.13491208655302741918",
      "media": {
        "url": "https://example.com/image.jpg",
        "caption": "Check out the July catalog"
      }
    }'
  ```

  ```javascript Node.js (axios) theme={null}
  const axios = require('axios')

  await axios.post(
    'https://api.zapsterapi.com/v1/wa/messages',
    {
      media: {
        caption: 'Check out the July catalog',
        url: 'https://example.com/image.jpg',
      },
      recipient: 'US.13491208655302741918',
    },
    {
      headers: {
        Authorization: 'Bearer YOUR_API_TOKEN',
        'X-Instance-ID': 'YOUR_INSTANCE_ID',
      },
    },
  )
  ```

  ```javascript Node.js (fetch) theme={null}
  await fetch('https://api.zapsterapi.com/v1/wa/messages', {
    body: JSON.stringify({
      media: {
        caption: 'Check out the July catalog',
        url: 'https://example.com/image.jpg',
      },
      recipient: 'US.13491208655302741918',
    }),
    headers: {
      Authorization: 'Bearer YOUR_API_TOKEN',
      'Content-Type': 'application/json',
      'X-Instance-ID': 'YOUR_INSTANCE_ID',
    },
    method: 'POST',
  })
  ```

  ```python Python theme={null}
  import requests

  requests.post(
      "https://api.zapsterapi.com/v1/wa/messages",
      headers={
          "Authorization": "Bearer YOUR_API_TOKEN",
          "X-Instance-ID": "YOUR_INSTANCE_ID",
      },
      json={
          "recipient": "US.13491208655302741918",
          "media": {
              "url": "https://example.com/image.jpg",
              "caption": "Check out the July catalog",
          },
      },
  )
  ```

  ```go Go theme={null}
  package main

  import (
  	"net/http"
  	"strings"
  )

  func main() {
  	body := strings.NewReader(`{
  		"recipient": "US.13491208655302741918",
  		"media": {
  			"url": "https://example.com/image.jpg",
  			"caption": "Check out the July catalog"
  		}
  	}`)

  	req, _ := http.NewRequest("POST", "https://api.zapsterapi.com/v1/wa/messages", body)
  	req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN")
  	req.Header.Set("X-Instance-ID", "YOUR_INSTANCE_ID")
  	req.Header.Set("Content-Type", "application/json")

  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	res.Body.Close()
  }
  ```
</CodeGroup>

### Interactive buttons

The WABA button rules also apply to BSUID: up to 3 `reply` buttons, or exactly 1 `url` button, without mixing the two types. The full matrix is in [Messages with buttons](/en/v1/guides/messages-with-buttons).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zapsterapi.com/v1/wa/messages \
    -H "Authorization: Bearer YOUR_API_TOKEN" \
    -H "X-Instance-ID: YOUR_INSTANCE_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "recipient": "US.13491208655302741918",
      "text": "Would you like to confirm tomorrow'\''s appointment?",
      "buttons": [
        { "id": "confirm", "label": "Confirm", "type": "reply" },
        { "id": "reschedule", "label": "Reschedule", "type": "reply" },
        { "id": "cancel", "label": "Cancel", "type": "reply" }
      ]
    }'
  ```

  ```javascript Node.js (axios) theme={null}
  const axios = require('axios')

  await axios.post(
    'https://api.zapsterapi.com/v1/wa/messages',
    {
      buttons: [
        { id: 'confirm', label: 'Confirm', type: 'reply' },
        { id: 'reschedule', label: 'Reschedule', type: 'reply' },
        { id: 'cancel', label: 'Cancel', type: 'reply' },
      ],
      recipient: 'US.13491208655302741918',
      text: 'Would you like to confirm tomorrow\'s appointment?',
    },
    {
      headers: {
        Authorization: 'Bearer YOUR_API_TOKEN',
        'X-Instance-ID': 'YOUR_INSTANCE_ID',
      },
    },
  )
  ```

  ```javascript Node.js (fetch) theme={null}
  await fetch('https://api.zapsterapi.com/v1/wa/messages', {
    body: JSON.stringify({
      buttons: [
        { id: 'confirm', label: 'Confirm', type: 'reply' },
        { id: 'reschedule', label: 'Reschedule', type: 'reply' },
        { id: 'cancel', label: 'Cancel', type: 'reply' },
      ],
      recipient: 'US.13491208655302741918',
      text: 'Would you like to confirm tomorrow\'s appointment?',
    }),
    headers: {
      Authorization: 'Bearer YOUR_API_TOKEN',
      'Content-Type': 'application/json',
      'X-Instance-ID': 'YOUR_INSTANCE_ID',
    },
    method: 'POST',
  })
  ```

  ```python Python theme={null}
  import requests

  requests.post(
      "https://api.zapsterapi.com/v1/wa/messages",
      headers={
          "Authorization": "Bearer YOUR_API_TOKEN",
          "X-Instance-ID": "YOUR_INSTANCE_ID",
      },
      json={
          "recipient": "US.13491208655302741918",
          "text": "Would you like to confirm tomorrow's appointment?",
          "buttons": [
              {"id": "confirm", "label": "Confirm", "type": "reply"},
              {"id": "reschedule", "label": "Reschedule", "type": "reply"},
              {"id": "cancel", "label": "Cancel", "type": "reply"},
          ],
      },
  )
  ```

  ```go Go theme={null}
  package main

  import (
  	"net/http"
  	"strings"
  )

  func main() {
  	body := strings.NewReader(`{
  		"recipient": "US.13491208655302741918",
  		"text": "Would you like to confirm tomorrow's appointment?",
  		"buttons": [
  			{ "id": "confirm", "label": "Confirm", "type": "reply" },
  			{ "id": "reschedule", "label": "Reschedule", "type": "reply" },
  			{ "id": "cancel", "label": "Cancel", "type": "reply" }
  		]
  	}`)

  	req, _ := http.NewRequest("POST", "https://api.zapsterapi.com/v1/wa/messages", body)
  	req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN")
  	req.Header.Set("X-Instance-ID", "YOUR_INSTANCE_ID")
  	req.Header.Set("Content-Type", "application/json")

  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	res.Body.Close()
  }
  ```
</CodeGroup>

### Template (outside the 24h window)

Templates work with BSUID, with one important exception: **one-tap, zero-tap, and copy-code authentication templates are not supported** for BSUID and are rejected by Meta with error `131062` (see [Limitations and errors](#limitations-and-errors)). Utility and marketing templates, and authentication templates with a text code, work normally.

The `template` object is a mirror of Meta's format. Details of `components`, positional variables, and named variables are in [Sending WhatsApp messages with WABA](/en/v1/guides/send-waba-messages#template-outside-the-24h-window).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.zapsterapi.com/v1/wa/messages \
    -H "Authorization: Bearer YOUR_API_TOKEN" \
    -H "X-Instance-ID: YOUR_INSTANCE_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "recipient": "US.13491208655302741918",
      "template": {
        "name": "order_confirmation",
        "language": "en_US",
        "components": [
          {
            "type": "body",
            "parameters": [
              { "type": "text", "text": "John" }
            ]
          }
        ]
      }
    }'
  ```

  ```javascript Node.js (axios) theme={null}
  const axios = require('axios')

  await axios.post(
    'https://api.zapsterapi.com/v1/wa/messages',
    {
      recipient: 'US.13491208655302741918',
      template: {
        components: [
          {
            parameters: [{ text: 'John', type: 'text' }],
            type: 'body',
          },
        ],
        language: 'en_US',
        name: 'order_confirmation',
      },
    },
    {
      headers: {
        Authorization: 'Bearer YOUR_API_TOKEN',
        'X-Instance-ID': 'YOUR_INSTANCE_ID',
      },
    },
  )
  ```

  ```javascript Node.js (fetch) theme={null}
  await fetch('https://api.zapsterapi.com/v1/wa/messages', {
    body: JSON.stringify({
      recipient: 'US.13491208655302741918',
      template: {
        components: [
          {
            parameters: [{ text: 'John', type: 'text' }],
            type: 'body',
          },
        ],
        language: 'en_US',
        name: 'order_confirmation',
      },
    }),
    headers: {
      Authorization: 'Bearer YOUR_API_TOKEN',
      'Content-Type': 'application/json',
      'X-Instance-ID': 'YOUR_INSTANCE_ID',
    },
    method: 'POST',
  })
  ```

  ```python Python theme={null}
  import requests

  requests.post(
      "https://api.zapsterapi.com/v1/wa/messages",
      headers={
          "Authorization": "Bearer YOUR_API_TOKEN",
          "X-Instance-ID": "YOUR_INSTANCE_ID",
      },
      json={
          "recipient": "US.13491208655302741918",
          "template": {
              "name": "order_confirmation",
              "language": "en_US",
              "components": [
                  {
                      "type": "body",
                      "parameters": [{"type": "text", "text": "John"}],
                  }
              ],
          },
      },
  )
  ```

  ```go Go theme={null}
  package main

  import (
  	"net/http"
  	"strings"
  )

  func main() {
  	body := strings.NewReader(`{
  		"recipient": "US.13491208655302741918",
  		"template": {
  			"name": "order_confirmation",
  			"language": "en_US",
  			"components": [
  				{
  					"type": "body",
  					"parameters": [{ "type": "text", "text": "John" }]
  				}
  			]
  		}
  	}`)

  	req, _ := http.NewRequest("POST", "https://api.zapsterapi.com/v1/wa/messages", body)
  	req.Header.Set("Authorization", "Bearer YOUR_API_TOKEN")
  	req.Header.Set("X-Instance-ID", "YOUR_INSTANCE_ID")
  	req.Header.Set("Content-Type", "application/json")

  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	res.Body.Close()
  }
  ```
</CodeGroup>

## Where the BSUID comes from

You do not invent a BSUID: it **arrives through incoming webhooks**. Zapster normalizes every webhook before delivering it, so you never deal with Meta's raw fields. The contact always arrives in the same shape, with `id`, `phone_number`, and `bsuid`.

In a `message.received` event, the customer's contact comes in `sender` (and, in one-to-one conversations, also in `recipient`, which mirrors `sender`). The `id` field is the primary identifier and follows a simple rule: **phone number when it exists, BSUID when it does not**.

* **Phone number visible:** `phone_number` carries the number and `bsuid` carries the BSUID (Meta sends both). `id` points to `phone_number`.
* **Phone number hidden (a privacy-conscious user):** `phone_number` is `null`, `bsuid` carries the BSUID, and `id` takes the BSUID value.

See `message.received` in both scenarios. In both, the contact's `id` is the value you use in `recipient` to reply within the 24-hour window:

<AccordionGroup>
  <Accordion title="Hidden phone number (BSUID only)">
    `phone_number` is `null`, `bsuid` carries the BSUID, and `id` takes the BSUID value.

    ```json theme={null}
    {
      "id": "V1StGXR8_Z5jdHi6B-myT",
      "type": "message.received",
      "created_at": "2026-07-25T14:22:07.000Z",
      "data": {
        "id": "wamid.HBgLMTU1NTU1NTU1NTUVAgARGBI5RjZ...",
        "type": "text",
        "sender": {
          "id": "US.13491208655302741918",
          "phone_number": null,
          "name": "Mary",
          "bsuid": "US.13491208655302741918",
          "profile_picture": null,
          "type": "chat"
        },
        "recipient": {
          "id": "US.13491208655302741918",
          "phone_number": null,
          "name": "Mary",
          "bsuid": "US.13491208655302741918",
          "profile_picture": null,
          "type": "chat"
        },
        "content": { "text": "Hi, where is my order?" },
        "sent_at": "2026-07-25T14:22:07.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="Visible phone number (phone + BSUID)">
    `phone_number` and `bsuid` are both filled in, and `id` points to `phone_number`.

    ```json theme={null}
    {
      "id": "V1StGXR8_Z5jdHi6B-myT",
      "type": "message.received",
      "created_at": "2026-07-25T14:22:07.000Z",
      "data": {
        "id": "wamid.HBgLMTU1NTU1NTU1NTUVAgARGBI5RjZ...",
        "type": "text",
        "sender": {
          "id": "5511999999999",
          "phone_number": "5511999999999",
          "name": "Mary",
          "bsuid": "US.13491208655302741918",
          "profile_picture": null,
          "type": "chat"
        },
        "recipient": {
          "id": "5511999999999",
          "phone_number": "5511999999999",
          "name": "Mary",
          "bsuid": "US.13491208655302741918",
          "profile_picture": null,
          "type": "chat"
        },
        "content": { "text": "Hi, where is my order?" },
        "sent_at": "2026-07-25T14:22:07.000Z"
      }
    }
    ```
  </Accordion>
</AccordionGroup>

In status events (`message.sent`, `message.delivered`, `message.read`), the customer's contact comes in `recipient` in the same shape, so the BSUID appears in `recipient.bsuid` (and in `recipient.id` when the phone number is hidden).

The practical flow is: the customer messages your number, you receive the webhook, you read the contact's `id` (which is already the BSUID when the phone number is hidden), you store that value, and you use it in the `recipient` of `POST /v1/wa/messages` to reply within the 24-hour window. See [Available events](/pt-BR/v1/webhooks/available-events) for the webhook catalog.

## Limitations and errors

When the payload uses a feature the Cloud API does not support with BSUID, Zapster rejects the request right away with a `400` error, without silently dropping anything.

| Code                                    | When it happens                                                                                             | How to fix                                                                                                                                         |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `waba_bsuid_requires_waba`              | A BSUID in the `recipient` of an **unofficial** instance                                                    | BSUID only works on the official channel (WABA). For unofficial instances, use the phone number (the analog of the BSUID on unofficial is the LID) |
| `waba_bsuid_message_type_not_supported` | A **one-tap**, **zero-tap**, or **copy-code** authentication template sent to a BSUID (Meta error `131062`) | These authentication templates require a phone number. Use a recipient with a phone number, or choose another message type                         |

On top of that, remember the per-portfolio scope:

* **A BSUID is scoped to a business portfolio.** A BSUID received in one portfolio does **not** work in another. If you try to send to a BSUID from an instance tied to a different portfolio, Meta rejects the send. Always reply using the same instance (same portfolio) that received that BSUID.

## Best practices

* **Accept and normalize the prefix casing.** The country code and the `ENT` segment are structural and canonically uppercase (`US`, `US.ENT.`). Zapster normalizes a BSUID typed as `us.ent.<id>` to the form Meta emits, so a lowercase value in `recipient` also works.
* **Never alter the identifier.** The part after the prefix is opaque and may be case-sensitive. Preserve it byte for byte: no uppercasing, no trimming of zeros, no transformation of any kind.
* **Store the canonical BSUID for correlation.** Save the BSUID in its canonical form (uppercase prefix, intact identifier) to match the value that arrives in webhooks and keep the contact's history consistent. Keep in mind that when the user changes their number, Meta generates a new BSUID for them, so treat the BSUID as the contact's identifier within that portfolio, not as something permanent.

## Next steps

* [Sending WhatsApp messages with WABA](/en/v1/guides/send-waba-messages) for the full guide on text, media, buttons, and templates
* [WABA vs unofficial](/en/v1/guides/waba-vs-unofficial) to understand the difference between BSUID (official) and LID (unofficial)
* [Available events](/pt-BR/v1/webhooks/available-events) to receive the BSUID in incoming webhooks
