Guides
Connect a WhatsApp account
Have your customers connect their WhatsApp Business account through Meta's Embedded Signup, then finalize the connection with the API.
Updated November 12, 2026View as Markdown
Each of your customers owns their own WhatsApp Business account. You connect it to your application in two steps: the customer authorizes the connection with Meta (in the browser), then your server finalizes it through the API.
The flow#
1. Your server POST /connected-businesses → creates your customer's business
2. The browser Meta Embedded Signup → the customer authorizes, Meta returns a code,
the WhatsApp account ID and the number ID
3. Your server POST …/waba-connections → exchanges the code, subscribes to events, reads the number
4. Your server GET …/phone-numbers → verifies: number, verified name, qualityCreate your customer's business#
curl -X POST "$INBOX_API/connected-businesses" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42"
}'const res = await fetch(`${process.env.INBOX_API}/connected-businesses`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42"
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/connected-businesses",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/connected-businesses');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Cabinet Dentaire Zokem',
'externalRef' => 'client-42',
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "39e2803f-79e7-4e99-8da0-1c15991f5985",
"name": "Cabinet Dentaire Zokem",
"externalRef": "client-42",
"createdAt": "2026-11-12T08:33:45.469Z"
}externalRef is your own identifier for this customer: it must be unique within the environment and lets you find the business later.
The customer completes signup with Meta#
In the browser, your customer signs in to Meta, then picks or creates their WhatsApp Business account and number. When it's done, Meta gives your page three values:
| Value | What it's for |
|---|---|
code | Exchanged by our servers for the customer's access token. Single-use and short-lived: send it right away. |
wabaId | The WhatsApp Business account ID (digits only). |
phoneNumberId | The Meta ID of the number (digits only). |
Finalize with the API#
curl -X POST "$INBOX_API/connected-businesses/BUSINESS_ID/waba-connections" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "AQBx7…",
"wabaId": "102290129340398",
"phoneNumberId": "106540352242922",
"mode": "cloud_api"
}'const res = await fetch(`${process.env.INBOX_API}/connected-businesses/BUSINESS_ID/waba-connections`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"code": "AQBx7…",
"wabaId": "102290129340398",
"phoneNumberId": "106540352242922",
"mode": "cloud_api"
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/connected-businesses/BUSINESS_ID/waba-connections",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"code": "AQBx7…",
"wabaId": "102290129340398",
"phoneNumberId": "106540352242922",
"mode": "cloud_api",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/connected-businesses/BUSINESS_ID/waba-connections');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'code' => 'AQBx7…',
'wabaId' => '102290129340398',
'phoneNumberId' => '106540352242922',
'mode' => 'cloud_api',
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "b76a1f98-53d6-4078-8d28-6406482daabb",
"wabaId": "102290129340398",
"status": "active",
"mode": "cloud_api",
"phoneNumber": {
"id": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"phoneNumberId": "106540352242922",
"displayNumber": "+237 6 52242922",
"verifiedName": "Cabinet Test",
"qualityRating": "GREEN"
}
}We exchange the code for a token, subscribe the application to the account's events, read the number's details and store everything. The response gives the connection id and the number ID (phoneNumber.id): this is the identifier you will use to send messages.
Verify#
curl "$INBOX_API/connected-businesses/BUSINESS_ID/phone-numbers" \
-H "Authorization: Bearer $INBOX_KEY"const res = await fetch(`${process.env.INBOX_API}/connected-businesses/BUSINESS_ID/phone-numbers`, {
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
},
});
const data = await res.json();import os
import requests
res = requests.get(
f"{os.environ['INBOX_API']}/connected-businesses/BUSINESS_ID/phone-numbers",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/connected-businesses/BUSINESS_ID/phone-numbers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);[
{
"id": "b76a1f98-53d6-4078-8d28-6406482daabb",
"wabaId": "102290129340398",
"status": "active",
"mode": "cloud_api",
"phoneNumbers": [
{
"id": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"phoneNumberId": "106540352242922",
"displayNumber": "+237 6 52242922",
"verifiedName": "Cabinet Test",
"qualityRating": "GREEN"
}
]
}
]qualityRating is the number's quality rating at Meta (GREEN, YELLOW, RED): keep an eye on it, as poor quality reduces your sending capacity.
Coexistence mode#
With "mode": "coexistence", your customer keeps using the WhatsApp Business app on their phone while your software uses the API on the same number. Messages they send from their phone reach you like any other. Without this mode (cloud_api, the default), the number can only be used through the API.
Multiple numbers for one business#
A business can have several WhatsApp accounts, and an account can have several numbers. To add a number, go through signup again and call POST …/waba-connections once more with the same wabaId and the new phoneNumberId: the account is recognized, its token refreshed and the number added, with no duplicates. Reconnecting a number that is already connected doesn't add one and doesn't count toward the limit.
In production, your plan limits the total number of numbers on your account (5 on Starter, 15 on Pro, 100 on Business), across all businesses: each additional number is added to the plan price (see Usage and billing). The sandbox has no limit.
What is guaranteed#
- Your customer's Meta token is never returned by the API, not even encrypted. It is stored encrypted, bound to your account and to this connection.
- A WhatsApp account can only be connected once across the entire platform. If it is already connected elsewhere, you get
409 conflict. This prevents a third party from hijacking another's messages. - Reconnecting the same account (expired token, new authorization) is possible: the token is replaced, with no duplicate number.
- A business only sees its own environment: an account connected in the sandbox doesn't exist in production.
Common errors#
| Error | Likely cause |
|---|---|
400 validation_error | wabaId or phoneNumberId contains something other than digits |
404 not_found | The business doesn't exist, or belongs to a different environment than the one the key is for |
409 conflict | This WhatsApp account is already connected (to another business or another account) |
402 plan_limit_reached | Your account's plan doesn't allow more WhatsApp numbers in production (tier cap), or, on the default plan, more connected businesses or numbers per business. The refusal happens before the signup code is exchanged: the code is not consumed |
502 meta_error | Meta rejected the request: most often, the code has expired or has already been used. Have the customer go through signup again |