# Connect WhatsApp

Connect a WhatsApp Business number to a profile with Embedded Signup or a Meta token, set its business profile and watch its quality and limits.

A WhatsApp account in Pozy is one WhatsApp Business number attached to a [profile](https://pozy.ai/docs/build/profiles). Once it is connected you send templates, replies, flows and broadcasts from it, take calls on it and read every conversation in the [inbox](https://pozy.ai/docs/inbox/messages). This page gets a number connected and ready to send.

## Before you start

- A Meta Business account. If it has no WhatsApp Business Account (WABA) yet, Meta creates one during signup.
- A phone number for WhatsApp. Either [buy one through Pozy](https://pozy.ai/docs/whatsapp/phone-numbers) or bring one you already own.
- A Pozy profile id and an API key.

A personal WhatsApp account cannot be connected. Only WhatsApp Business numbers on Meta's Cloud API can.

Two WhatsApp rules apply to everything you send:

| Rule | What it means for you |
| --- | --- |
| 24-hour customer service window | After a customer messages you, you can reply with anything for 24 hours. Outside that window the first message must be an approved [template](https://pozy.ai/docs/whatsapp/templates). |
| Messaging limit tier | Meta caps how many unique people a number can start conversations with per day. A new number starts at `TIER_250` (250 per day). Meta raises the tier as the number builds history with good quality. |

## Pick the kind of number

Where the number comes from decides what else it can do.

| Number | How it connects | Calls and SMS on the same number |
| --- | --- | --- |
| Bought or ported through Pozy | Embedded Signup. The number is already verified and shows as **Verified** in Meta's picker. | Yes |
| Your own number on your own carrier | Embedded Signup, or a Meta System User token | No |

A number bought through Pozy is pinned to the profile you bought it for. Connecting it from another profile returns `409`. Move it first with [`PATCH /v1/whatsapp/phone-numbers/{id}/profile`](https://pozy.ai/docs/api/moveWhatsAppNumberToProfile).

A profile can hold several WhatsApp numbers, and each one becomes its own account. A number can only be live on one profile at a time.

## Cloud API only or coexistence

Meta lets a business keep using the WhatsApp Business app on the same number while the API is connected. Meta calls this coexistence. It is convenient, but it switches features off.

| Feature | Cloud API only | Coexistence |
| --- | --- | --- |
| Throughput | Meta's standard tiers | Fixed at 20 messages per second |
| [Group chats](https://pozy.ai/docs/whatsapp/groups) | Yes | No |
| [Calling](https://pozy.ai/docs/whatsapp/calling) | Yes | No |
| Profile photo | Managed through the API | Locked, managed in the app |
| Message history | Starts at connection | Up to 6 months synced from the app |
| Sends from the phone | Not possible | Allowed, and reported on `message.sent` with `source: "whatsapp_business_app"` |

Pass `onboarding=api` to connect Cloud API only. Leave it out, or pass `onboarding=business_app`, to offer coexistence. With coexistence the person connecting scans a QR code from the WhatsApp Business app during signup, so they need the phone with them.

> **Warning:**
> 
> Meta has no API to leave coexistence. The business disconnects it in the app (**Settings > Account > Business Platform > Disconnect**). Pozy then deactivates the account and sends `account.disconnected`. To get groups and calling afterwards, connect the number again with `onboarding=api`.

## Connect with Embedded Signup

Embedded Signup is Meta's own signup window. You ask Pozy for a URL, send the person there, and they come back to your `redirect_url` with the new account id.

```bash cURL
curl "https://api.pozy.ai/v1/connect/whatsapp?profileId=6710c4e2b9a1f03d8e5c7a21&redirect_url=https://app.example.com/whatsapp/done&onboarding=api" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/whatsapp?profileId=6710c4e2b9a1f03d8e5c7a21&redirect_url=https:%2F%2Fapp.example.com%2Fwhatsapp%2Fdone&onboarding=api", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

// Send the browser here
console.log(data.authUrl);
```
```python Python
import os
import requests

resp = requests.get(
    "https://api.pozy.ai/v1/connect/whatsapp",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={
        "profileId": "6710c4e2b9a1f03d8e5c7a21",
        "redirect_url": "https://app.example.com/whatsapp/done",
        "onboarding": "api",
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["authUrl"])
```

The person logs in to Meta, picks or creates a WABA and picks the number. They land back on your page with the result in the query string:

```text
https://app.example.com/whatsapp/done?connected=whatsapp&profileId=6710c4e2b9a1f03d8e5c7a21&accountId=6712a9d4c0e3b58f1a7d2e64&username=%2B14155550142
```

Store `accountId`. Every WhatsApp endpoint takes it.

If signup fails, the redirect carries `error` and `platform=whatsapp` instead:

| `error` | Meaning |
| --- | --- |
| `connection_cancelled` | The person closed Meta's window before finishing. |
| `one_whatsapp_per_profile` | The profile rules allow no more WhatsApp numbers here. Use another profile. |
| `whatsapp_number_pinned_to_profile` | A Pozy number that belongs to another profile. Connect it there or move it first. |
| `whatsapp_number_already_connected` | The number is live on another profile or workspace. Disconnect it there first. |
| `payment_required` | The workspace needs a payment method. |
| `whatsapp_error` | Meta refused the signup. Meta's reason, when given, is in `error_message`. |

### Let Pozy host the signup page

When one Facebook login manages several WhatsApp accounts (agency staff, a business with several brands), the redirect flow above can only learn that the person authorized, not which number they picked in Meta's window. Pozy then asks them to pick the number a second time.

Add `signup=hosted` and `authUrl` points to a Pozy page instead of Meta's dialog. That page opens Meta's window itself, reads which number the person chose and connects exactly that one. You embed nothing and register no domain with Meta.

| Parameter | Effect | Rules |
| --- | --- | --- |
| `brandName` | Page title reads "Connect your WhatsApp number to *brandName*" | 1 to 60 characters |
| `primaryColor` | Colour of the main button and step accents | Hex `#RRGGBB` |
| `language` | Language of the page and its checklist | `en` (default), `es` or `pt-BR` |

These three only work with `signup=hosted`. Anywhere else they return `400`. The hosted page stays valid for 60 minutes; after that the person lands on your `redirect_url` with `error=session_expired` and you request a new URL.

### Pick the number in your own UI

Pass `headless=true` if you want to show your own number picker. When the chosen WABA holds two or more numbers, the person lands on your `redirect_url` with `step=select_phone_number`, `profileId` and a short-lived `tempToken`. A WABA with one number connects straight away and skips this step.

```bash cURL
# 1. List the numbers the person can pick from
curl "https://api.pozy.ai/v1/connect/whatsapp/select-phone-number?profileId=6710c4e2b9a1f03d8e5c7a21&tempToken=$TEMP_TOKEN" \
  -H "Authorization: Bearer $POZY_API_KEY"

# 2. Connect the one they chose
curl -X POST https://api.pozy.ai/v1/connect/whatsapp/select-phone-number \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6710c4e2b9a1f03d8e5c7a21",
    "phoneNumberId": "109876543210987",
    "wabaId": "204567890123456",
    "tempToken": "'"$TEMP_TOKEN"'"
  }'
```
```ts Node
const profileId = "6710c4e2b9a1f03d8e5c7a21";
const tempToken = new URL(callbackUrl).searchParams.get("tempToken")!;

const listRes = await fetch(`https://api.pozy.ai/v1/connect/whatsapp/select-phone-number?${new URLSearchParams({ profileId, tempToken })}`, {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const list = await listRes.json();
if (!listRes.ok) throw new Error(list.error);

const chosen = list.phoneNumbers[0]; // show these in your own picker
const res = await fetch("https://api.pozy.ai/v1/connect/whatsapp/select-phone-number", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ profileId, phoneNumberId: chosen.id, wabaId: chosen.wabaId, tempToken }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.account.accountId);
```
```python Python
base = "https://api.pozy.ai/v1/connect/whatsapp/select-phone-number"
headers = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
profile_id = "6710c4e2b9a1f03d8e5c7a21"

numbers = requests.get(
    base, headers=headers,
    params={"profileId": profile_id, "tempToken": temp_token}, timeout=30,
).json()["phoneNumbers"]

chosen = numbers[0]
resp = requests.post(
    base, headers=headers, timeout=30,
    json={"profileId": profile_id, "phoneNumberId": chosen["id"],
          "wabaId": chosen["wabaId"], "tempToken": temp_token},
)
print(resp.json()["account"]["accountId"])
```

Each listed number includes its `display_phone_number`, `verified_name`, `quality_rating`, `name_status` and `messaging_limit_tier`, which is enough to build a useful picker.

## Connect with a Meta token (no browser)

If you already manage the business in Meta, connect from your server with a permanent System User token. This suits provisioning scripts and server-to-server setups.

1. In Meta Business Suite, create a System User and generate a token with `whatsapp_business_management` and `whatsapp_business_messaging`. Add `whatsapp_business_manage_events` if you want to send [Click-to-WhatsApp conversions](#measure-click-to-whatsapp-ads).
2. Copy the WABA id and the phone number id from WhatsApp Manager.
3. Call [`POST /v1/connect/whatsapp/credentials`](https://pozy.ai/docs/api/connectWhatsAppCredentials). Add `pin` if the number has two-step verification turned on.

```bash cURL
curl -X POST https://api.pozy.ai/v1/connect/whatsapp/credentials \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6710c4e2b9a1f03d8e5c7a21",
    "accessToken": "EAAG...system-user-token",
    "wabaId": "204567890123456",
    "phoneNumberId": "109876543210987",
    "pin": "205718"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/whatsapp/credentials", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "6710c4e2b9a1f03d8e5c7a21",
    accessToken: process.env.META_SYSTEM_USER_TOKEN!,
    wabaId: "204567890123456",
    phoneNumberId: "109876543210987",
    pin: "205718",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.account.accountId, data.account.qualityRating);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/connect/whatsapp/credentials",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "profileId": "6710c4e2b9a1f03d8e5c7a21",
        "accessToken": os.environ["META_SYSTEM_USER_TOKEN"],
        "wabaId": "204567890123456",
        "phoneNumberId": "109876543210987",
        "pin": "205718",
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["account"]["accountId"])
```

Pozy checks the token with Meta, creates the account, subscribes the WABA to message events and registers the number. If `phoneNumberId` is not in that WABA, the `400` lists the numbers that are.

> **Existing webhooks on the WABA stop:**
> 
> Connecting routes the WABA's events to Pozy. Any callback URL you had set on that WABA stops receiving events right away. Do not unsubscribe your Meta app from the WABA afterwards, or Pozy stops receiving messages too and you have to connect again.

## Set the business profile

The business profile is what customers see when they open your chat. Update only the fields you send.

```bash cURL
curl -X POST https://api.pozy.ai/v1/whatsapp/business-profile \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6712a9d4c0e3b58f1a7d2e64",
    "about": "Fresh bread, baked every morning",
    "description": "Neighbourhood bakery in Lisbon. Order by 6pm for next-day pickup.",
    "email": "hello@example.com",
    "websites": ["https://example.com"]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/whatsapp/business-profile", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6712a9d4c0e3b58f1a7d2e64",
    about: "Fresh bread, baked every morning",
    description: "Neighbourhood bakery in Lisbon. Order by 6pm for next-day pickup.",
    email: "hello@example.com",
    websites: ["https://example.com"],
  }),
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
requests.post(
    "https://api.pozy.ai/v1/whatsapp/business-profile",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6712a9d4c0e3b58f1a7d2e64",
        "about": "Fresh bread, baked every morning",
        "description": "Neighbourhood bakery in Lisbon. Order by 6pm for next-day pickup.",
        "email": "hello@example.com",
        "websites": ["https://example.com"],
    },
    timeout=30,
).raise_for_status()
```

| Field | Limit |
| --- | --- |
| `about` | 139 characters |
| `description` | 512 characters |
| `websites` | 2 URLs |
| `vertical` | One of Meta's business categories |

The profile photo, display name and WhatsApp username have their own endpoints:

- **Photo:** [`POST /v1/whatsapp/business-profile/photo`](https://pozy.ai/docs/api/uploadWhatsAppProfilePhoto). Locked on coexistence numbers.
- **Display name:** request a change with [`POST /v1/whatsapp/business-profile/display-name`](https://pozy.ai/docs/api/updateWhatsAppDisplayName). Meta reviews every change, so the name starts as `PENDING_REVIEW`. The verdict arrives as `whatsapp.account.name_status_updated` (`APPROVED`, `DECLINED` or `PENDING_REVIEW`), and [`GET .../display-name`](https://pozy.ai/docs/api/getWhatsAppDisplayName) reads the current state.
- **Username:** reserve a public username of up to 35 characters with [`POST /v1/whatsapp/business-profile/username`](https://pozy.ai/docs/api/setWhatsappBusinessUsername). [Suggestions](https://pozy.ai/docs/api/getWhatsappBusinessUsernameSuggestions) returns names that are free.

## Watch quality and limits

Meta rates every number on how recipients react to its messages. A falling rating can lower the messaging limit or get templates paused.

[`GET /v1/whatsapp/number-info`](https://pozy.ai/docs/api/getWhatsAppNumberInfo) reads the live state from Meta:

| Field | Values |
| --- | --- |
| `phone.quality_rating` | `GREEN`, `YELLOW`, `RED` or `UNKNOWN` |
| `phone.messaging_limit_tier` | `TIER_250` for a new number, then `TIER_2K`, `TIER_10K`, `TIER_100K`, `TIER_UNLIMITED` |
| `phone.name_status` | Display name approval state |
| `phone.status` | Meta's connection status, for example `CONNECTED` |
| `waba.business_verification_status` | Whether Meta has verified the business behind the WABA |

You do not have to poll. Pozy sends `whatsapp.account.quality_updated` when the rating or the tier changes, with both the old and the new value, and `whatsapp.account.status_updated` when Meta restricts or reinstates the WABA. A restriction lists what was blocked (for example `RESTRICTED_BIZ_INITIATED_MESSAGING`) and when it lifts.

Business verification happens in Meta Business Manager, not through the API. Some features, such as conversion tracking for Click-to-WhatsApp ads, need a verified business. Read `waba.business_verification_status` before you turn them on.

[Phone numbers for WhatsApp](https://pozy.ai/docs/whatsapp/phone-numbers#check-that-a-number-is-alive) explains how to tell a dead channel from a healthy one.

## Ice breakers and slash commands

Give people something to tap when they open the chat. [`POST /v1/whatsapp/conversational-automation`](https://pozy.ai/docs/api/setWhatsAppConversationalAutomation) sets:

- up to 3 ice breaker `prompts`, 80 characters each
- up to 30 slash `commands` (`command_name` up to 30 characters, `command_description` up to 256)
- `enable_welcome_message`, which lets Meta greet first-time visitors

A tapped prompt arrives as an ordinary `message.received` with the prompt text, so a keyword [workflow](https://pozy.ai/docs/automations/workflows) can answer it.

## Measure Click-to-WhatsApp ads

When a conversation starts from a Click-to-WhatsApp ad, Meta attaches a click id to the first message. Pozy stores it on the conversation so you can report results back to Meta and see which ad paid off.

1. Create the dataset once per WABA with [`POST /v1/whatsapp/dataset`](https://pozy.ai/docs/api/createWhatsAppDataset). The call is safe to repeat.
2. When something happens in the chat, send it with [`POST /v1/whatsapp/conversions`](https://pozy.ai/docs/api/sendWhatsAppConversion): `eventName` (`LeadSubmitted`, `QualifiedLead`, `Purchase`, `AddToCart`, `InitiateCheckout`, `ViewContent`, `OrderCreated`, `OrderCanceled` or `OrderReturned`), a stable `eventId` and the `conversationId`.

Meta attributes events for 7 days after the click. Create the ads themselves from [Meta Ads](https://pozy.ai/docs/ads/meta-ads).

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/{platform}`](https://pozy.ai/docs/api/getConnectUrl) | Get OAuth connect URL |
| [`GET /v1/connect/whatsapp/select-phone-number`](https://pozy.ai/docs/api/listWhatsAppPhoneNumbers) | List numbers for selection |
| [`POST /v1/connect/whatsapp/select-phone-number`](https://pozy.ai/docs/api/completeWhatsAppPhoneSelection) | Complete number selection |
| [`POST /v1/connect/whatsapp/credentials`](https://pozy.ai/docs/api/connectWhatsAppCredentials) | Connect WhatsApp via credentials |
| [`POST /v1/connect/whatsapp/embedded-signup`](https://pozy.ai/docs/api/connectWhatsAppEmbeddedSignup) | Connect WhatsApp from Embedded Signup |
| [`GET /v1/connect/whatsapp/sdk-config`](https://pozy.ai/docs/api/getWhatsAppSdkConfig) | Get Embedded Signup SDK config |
| [`GET /v1/whatsapp/business-profile`](https://pozy.ai/docs/api/getWhatsAppBusinessProfile) | Get business profile |
| [`POST /v1/whatsapp/business-profile`](https://pozy.ai/docs/api/updateWhatsAppBusinessProfile) | Update business profile |
| [`POST /v1/whatsapp/business-profile/photo`](https://pozy.ai/docs/api/uploadWhatsAppProfilePhoto) | Upload profile picture |
| [`GET /v1/whatsapp/business-profile/display-name`](https://pozy.ai/docs/api/getWhatsAppDisplayName) | Get display name status |
| [`POST /v1/whatsapp/business-profile/display-name`](https://pozy.ai/docs/api/updateWhatsAppDisplayName) | Request display name change |
| [`GET /v1/whatsapp/business-profile/username`](https://pozy.ai/docs/api/getWhatsappBusinessUsername) | Get business username |
| [`POST /v1/whatsapp/business-profile/username`](https://pozy.ai/docs/api/setWhatsappBusinessUsername) | Set business username |
| [`DELETE /v1/whatsapp/business-profile/username`](https://pozy.ai/docs/api/deleteWhatsappBusinessUsername) | Delete business username |
| [`GET /v1/whatsapp/business-profile/username/suggestions`](https://pozy.ai/docs/api/getWhatsappBusinessUsernameSuggestions) | Get username suggestions |
| [`GET /v1/whatsapp/number-info`](https://pozy.ai/docs/api/getWhatsAppNumberInfo) | Get number status |
| [`GET /v1/whatsapp/conversational-automation`](https://pozy.ai/docs/api/getWhatsAppConversationalAutomation) | Get ice breakers and commands |
| [`POST /v1/whatsapp/conversational-automation`](https://pozy.ai/docs/api/setWhatsAppConversationalAutomation) | Set ice breakers and commands |
| [`DELETE /v1/whatsapp/conversational-automation`](https://pozy.ai/docs/api/deleteWhatsAppConversationalAutomation) | Clear ice breakers and commands |
| [`GET /v1/whatsapp/block-users`](https://pozy.ai/docs/api/getWhatsAppBlockedUsers) | List blocked users |
| [`GET /v1/whatsapp/block-users/status`](https://pozy.ai/docs/api/getWhatsAppBlockStatus) | Check if a user is blocked |
| [`POST /v1/whatsapp/block-users`](https://pozy.ai/docs/api/blockWhatsAppUsers) | Block users |
| [`DELETE /v1/whatsapp/block-users`](https://pozy.ai/docs/api/unblockWhatsAppUsers) | Unblock users |
| [`GET /v1/whatsapp/account-events`](https://pozy.ai/docs/api/listWhatsAppAccountEvents) | List account notifications |
| [`GET /v1/whatsapp/dataset`](https://pozy.ai/docs/api/getWhatsAppDataset) | Get CTWA conversions dataset |
| [`POST /v1/whatsapp/dataset`](https://pozy.ai/docs/api/createWhatsAppDataset) | Provision CTWA dataset |
| [`GET /v1/whatsapp/conversions`](https://pozy.ai/docs/api/listWhatsAppConversions) | List conversion events |
| [`POST /v1/whatsapp/conversions`](https://pozy.ai/docs/api/sendWhatsAppConversion) | Send WhatsApp conversion event |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`account.connected`](https://pozy.ai/docs/webhooks/events#account-connected) | Account connected. |
| [`account.disconnected`](https://pozy.ai/docs/webhooks/events#account-disconnected) | Account disconnected. |
| [`whatsapp.account.quality_updated`](https://pozy.ai/docs/webhooks/events#whatsapp-account-quality-updated) | WhatsApp quality rating or messaging limit changed. |
| [`whatsapp.account.status_updated`](https://pozy.ai/docs/webhooks/events#whatsapp-account-status-updated) | WhatsApp Business Account restricted or reinstated. |
| [`whatsapp.account.alert_received`](https://pozy.ai/docs/webhooks/events#whatsapp-account-alert-received) | WhatsApp account alert received. |
| [`whatsapp.contact.identity_changed`](https://pozy.ai/docs/webhooks/events#whatsapp-contact-identity-changed) | WhatsApp contact identity changed. |
| [`whatsapp.account.name_status_updated`](https://pozy.ai/docs/webhooks/events#whatsapp-account-name-status-updated) | WhatsApp display-name review outcome. |
| [`whatsapp.automatic_event`](https://pozy.ai/docs/webhooks/events#whatsapp-automatic-event) | WhatsApp automatic event detected. |
