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. Once it is connected you send templates, replies, flows and broadcasts from it, take calls on it and read every conversation in the inbox. 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 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. |
| 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.
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 | Yes | No |
| 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.
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.
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"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);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:
https://app.example.com/whatsapp/done?connected=whatsapp&profileId=6710c4e2b9a1f03d8e5c7a21&accountId=6712a9d4c0e3b58f1a7d2e64&username=%2B14155550142Store 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.
# 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"'"
}'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);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.
- In Meta Business Suite, create a System User and generate a token with
whatsapp_business_managementandwhatsapp_business_messaging. Addwhatsapp_business_manage_eventsif you want to send Click-to-WhatsApp conversions. - Copy the WABA id and the phone number id from WhatsApp Manager.
- Call
POST /v1/connect/whatsapp/credentials. Addpinif the number has two-step verification turned on.
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"
}'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);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.
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.
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"]
}'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);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. Locked on coexistence numbers. - Display name: request a change with
POST /v1/whatsapp/business-profile/display-name. Meta reviews every change, so the name starts asPENDING_REVIEW. The verdict arrives aswhatsapp.account.name_status_updated(APPROVED,DECLINEDorPENDING_REVIEW), andGET .../display-namereads the current state. - Username: reserve a public username of up to 35 characters with
POST /v1/whatsapp/business-profile/username. Suggestions 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 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 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 sets:
- up to 3 ice breaker
prompts, 80 characters each - up to 30 slash
commands(command_nameup to 30 characters,command_descriptionup 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 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.
- Create the dataset once per WABA with
POST /v1/whatsapp/dataset. The call is safe to repeat. - When something happens in the chat, send it with
POST /v1/whatsapp/conversions:eventName(LeadSubmitted,QualifiedLead,Purchase,AddToCart,InitiateCheckout,ViewContent,OrderCreated,OrderCanceledorOrderReturned), a stableeventIdand theconversationId.
Meta attributes events for 7 days after the click. Create the ads themselves from Meta Ads.
#Endpoints
#Webhook events
| Event | When it fires |
|---|---|
account.connected |
Account connected. |
account.disconnected |
Account disconnected. |
whatsapp.account.quality_updated |
WhatsApp quality rating or messaging limit changed. |
whatsapp.account.status_updated |
WhatsApp Business Account restricted or reinstated. |
whatsapp.account.alert_received |
WhatsApp account alert received. |
whatsapp.contact.identity_changed |
WhatsApp contact identity changed. |
whatsapp.account.name_status_updated |
WhatsApp display-name review outcome. |
whatsapp.automatic_event |
WhatsApp automatic event detected. |