Onboard your customer's number
Send your customer to a hosted WhaBuzz page that connects their WhatsApp number, then verify the result on your server. Redirect or embedded modal, with webhooks and the states a session moves through.
A connect session is a one-time link to a WhaBuzz-hosted page where your customer connects their own WhatsApp number to your account. You create the session, send the customer to the link, and learn the outcome from the return redirect and a webhook. The customer never needs a WhaBuzz login, and Meta's access token never reaches your server.
Use it when you run a product for other businesses and each of them brings a number. If you connect your own number, use the console. If an app of yours needs access to a number a WhaBuzz customer already owns, use Connect your app.
#Who does what
- Each business completes Embedded Signup itself, under its own Meta Business Portfolio. You never sign up on a business's behalf, and the link must go to the business, not be opened by you.
- WhaBuzz is the provider of record to Meta and is answerable for every business connected through it. So you sell your own service, never resold WhaBuzz access.
- The Facebook dialog in Embedded Signup names WhaBuzz as the app, whatever
brandingthe session carries. - Each business verifies itself with Meta. WhaBuzz cannot do it for them. See
business_verification_statusin the result. - The business still gets Meta's own mails (business verification, payment method) from Meta directly, which WhaBuzz does not control.
- Meta allows WhaBuzz 200 new businesses per rolling 7 days across all owners, which is one reason tenant limits exist.
You can also make a link from the console: on the Tenants page, Connect link creates a session for an active tenant, copies the link, and you send it to the business.
#Quickstart
#1. Create a session
With a tenant API key (or an app key plus X-WhaBuzz-Tenant). A key bound to one number is refused with 403.
curl -X POST https://api.whabuzz.com/v1/connect-sessions \
-H "Authorization: Bearer $WHABUZZ_KEY" -H "Content-Type: application/json" \
-d '{
"return_url": "https://app.example.com/whatsapp/done",
"cancel_url": "https://app.example.com/whatsapp",
"branding": { "product_name": "Acme CRM", "accent": "#1F6FEB" }
}'{
"id": "cs_01JAB3N2R5T8W1Y4A7C0E3G6J9",
"url": "https://app.whabuzz.com/connect/s/9fK2mQ7xL4pV1sN8cR3t…",
"expires_at": "2026-10-22T10:15:00.000Z",
"state": "created"
}url is the only copy of the link. Store the id; send the customer to url.
| Field | Meaning |
|---|---|
return_url | Required. https, at most 2048 characters, no credentials, no #fragment. |
cancel_url | Optional. Where a customer who backs out goes. Defaults to return_url. |
branding.product_name | 1 to 60 characters. Shown on the page and in "Return to …". |
branding.logo_url | https URL of your logo. |
branding.accent | Hex colour such as #1F6FEB. |
options.coexistence | true to connect a number that also keeps running on the WhatsApp Business app. See Coexistence. |
options.allow_template_import | Default true. Offer to copy approved templates from the business's other WhatsApp accounts. |
options.existing_number_e164 | The number you expect them to connect, such as +919876543210. |
allowed_origins | Up to 5 bare https origins that may embed the page (embedded mode). Defaults to the origin of return_url. whabuzz.com is never allowed. |
#2. Send the customer to the link
Redirect the browser to url. The link is one-time and must be opened within 15 minutes. Opening it binds that browser to the session for 24 hours. A second open answers 410, so do not let link scanners or previews fetch it.
The customer then sees:
- Meta's Embedded Signup popup.
- A phone re-verification step, only if Meta asks for one.
- An offer to import templates from the business's other WhatsApp accounts, if you allowed it.
- A payment-method step (the WhatsApp Manager billing page). See billing_state.
- Return to your product.
#3. Handle the return and verify it
The customer arrives at return_url (or cancel_url after a cancel) with three query parameters:
https://app.example.com/whatsapp/done?session_id=cs_01JAB…&state=completed&result_token=1761128400.Xq…Do not trust the query string. Anyone can type it. Confirm it from your server:
curl "https://api.whabuzz.com/v1/connect-sessions/cs_01JAB3N2R5T8W1Y4A7C0E3G6J9?result_token=1761128400.Xq…" \
-H "Authorization: Bearer $WHABUZZ_KEY"{
"id": "cs_01JAB3N2R5T8W1Y4A7C0E3G6J9",
"state": "completed",
"result": {
"number_id": "6f1c2d9e-3b4a-4c5d-8e7f-0a1b2c3d4e5f",
"phone_e164": "+919876543210",
"phone_number_id": "1013622581841046",
"waba_id": "26059971920328384",
"sibling_accounts": null,
"templates_imported": 4,
"billing_state": null
},
"error": null,
"attempts": 1,
"result_token": { "valid": true, "state": "completed" }
}Trust state and result from this response only when result_token.valid is true. A forged, malformed or expired token (they last 10 minutes) answers 400 invalid_result_token; read the session without the token instead. Then save result.phone_number_id (and number_id) against your customer.
In Node:
const s = await wb.connectSessions.get(sessionId, { resultToken });
if (s.result_token?.valid && s.state === 'completed') await saveNumber(customerId, s.result.phone_number_id);#4. Listen for the webhook
The return redirect only happens if the customer's browser makes it back. Subscribe an endpoint to connect.completed and connect.failed (events) so you hear about every session that ended, with or without the redirect. Each session sends at most one event. Never-opened sessions that expire, and cancelled sessions, send nothing.
#Embedded mode
To keep the customer on your page, open the link in a modal instead of redirecting:
<script src="https://app.whabuzz.com/v1/connect.js"></script>
<script>
// url: the `url` your server got from POST /v1/connect-sessions
WhaBuzz.connect({
url,
onComplete({ session_id, state, result_token }) {
// Send all three to your server and verify them as in step 3.
fetch('/whatsapp/verify', { method: 'POST', body: JSON.stringify({ session_id, state, result_token }) });
},
onCancel({ session_id, state, result_token }) {
// The customer closed the modal without connecting.
},
});
</script>- The iframe loads only on pages whose origin is in the session's
allowed_origins. Create the session with the origin of the page that will embed it. - Messages between your page and the modal are checked by origin on both sides. The callbacks give you the same three values as the redirect; verify them the same way.
- On mobile, or when the browser blocks popups or iframes,
connect.jsfalls back to a full-page redirect. The customer then returns throughreturn_url, so keep that route working.
If your site sets a Content-Security-Policy, allow WhaBuzz in these directives:
script-src https://app.whabuzz.com;
frame-src https://app.whabuzz.com;#Direct mode: Meta's popup on your page
To skip the WhaBuzz page entirely, run Meta's Embedded Signup popup on your own page and hand the result to WhaBuzz from your server. This needs your domain on the WhaBuzz Meta app's allowed domains first: ask WhaBuzz support. Until then, Meta refuses the popup on your page, so keep the hosted url as the fallback.
- Create the session with
"mode": "direct". The answer carries the hostedurl(keep it for the fallback) and anembedded_signupblock:
{
"id": "cs_01JAB3N2R5T8W1Y4A7C0E3G6J9",
"url": "https://app.whabuzz.com/connect/s/9fK2mQ7xL4pV1sN8cR3t…",
"state": "created",
"embedded_signup": {
"es_enabled": true,
"meta_app_id": "28719689174328309",
"config_id": "1021969937569247",
"graph_version": "v26.0",
"es_version": "v4",
"feature_type": null
}
}- Load Meta's JavaScript SDK before the customer clicks, then call
FB.logininside the click (browsers block popups opened later):
FB.init({ appId: es.meta_app_id, version: es.graph_version, xfbml: false });
FB.login(onLogin, {
config_id: es.config_id, response_type: 'code', override_default_response_type: true,
extras: es.es_version === 'v2'
? { setup: {}, sessionInfoVersion: '3' }
: es.feature_type ? { featureType: es.feature_type } : {},
});Listen for message events from an origin ending in facebook.com whose type is WA_EMBEDDED_SIGNUP. FINISH (or FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING for coexistence) carries waba_id and phone_number_id; CANCEL carries the step the customer left at.
- Send the code and the two ids to your server, and from there:
curl -X POST https://api.whabuzz.com/v1/connect-sessions/cs_01JAB3N2R5T8W1Y4A7C0E3G6J9/complete \
-H "Authorization: Bearer $WHABUZZ_KEY" -H "Content-Type: application/json" \
-d '{ "code": "AQBx…", "waba_id": "26059971920328384", "phone_number_id": "1013622581841046" }'The answer is the session:
state | Do this |
|---|---|
completed | Save result.phone_number_id. connect.completed is sent. Template import and the payment-method step are yours to offer. |
needs_verification | Send the customer to the hosted url to verify the phone. The link still opens once, within 15 minutes of creating the session. |
opened with error | The attempt failed; error.message has Meta's words. Let them try again (up to 5 attempts), or offer the hosted url. |
failed | Five attempts failed. connect.failed is sent. Create a new session. |
- The code is single use. WhaBuzz exchanges it with its own app secret and never stores or logs it.
- Calling
completeagain on a completed session answers it again without touching Meta. 409 not_direct: the session was created for the hosted page.409 invalid_state: it is already connecting, waiting for verification, or over.- Content-Security-Policy for the popup:
script-src https://connect.facebook.net; frame-src https://www.facebook.com https://*.facebook.com;.
#States
| State | Meaning | Final |
|---|---|---|
created | The link exists and has not been opened. | No |
opened | The customer opened it. The browser is bound for 24 hours. | No |
connecting | Meta's signup finished; WhaBuzz is connecting the number. | No |
needs_verification | Meta wants the number verified by SMS or voice call. | No |
connected | The number is connected. The customer may still import templates or set up billing. | No |
importing | Templates are being copied from another WhatsApp account. | No |
completed | The customer finished, or left a connected session idle for 30 minutes. | Yes |
failed | Five connect attempts failed. error says why. | Yes |
expired | The link was not opened in 15 minutes, or the bound browser did not finish in 24 hours. | Yes |
cancelled | The customer backed out before connecting. | Yes |
A failed attempt returns the session to opened so the customer can try again, up to 5 attempts (attempts). A customer who leaves after the number connected is completed, not cancelled: the number is yours. A 10-minute sweep completes idle connected sessions and expires abandoned ones.
#Result
result is null until a number is connected.
| Field | Meaning |
|---|---|
number_id | WhaBuzz's internal id for the number (a UUID). |
phone_e164 | The number, such as +919876543210. |
phone_number_id | Meta's id for the number. Use it with /v1/numbers and as from when sending. |
waba_id | The WhatsApp Business Account the number belongs to. |
sibling_accounts | The business's other WhatsApp accounts and their approved templates, or null when not checked. See Offer to import templates. |
templates_imported | Templates copied during this session. |
business_verification_status | Meta's verification state of the business portfolio, or null when not read yet. verified when done. Anything else: the business verifies itself in Meta Business Manager, and its messaging limits stay lower until it does. The hosted page tells the business this on its last step. |
billing_state | May be null. Meta delivers business-initiated (template) messages only after the WhatsApp account has a payment method; until then Meta answers error 131042. Check this field before your first template send. |
#Webhooks
| Event | Sent when |
|---|---|
connect.completed | The session reached completed. |
connect.failed | The session reached failed, or expired after the link was opened. |
data carries session_id, state, result, error and product_name. Number fields in the envelope (phone_number_id, display_phone_number, waba_id) are null when no number connected. Full payloads: WhaBuzz events.
#Security
- The link is a credential. It is single-use, and WhaBuzz keeps only a hash of it. Do not log it.
- The browser is bound. After the first open, only that browser can continue the session, for 24 hours.
- Verify every return. The
result_tokenis a signed value for one session and one final state, valid 10 minutes. Always callGET /v1/connect-sessions/{id}?result_token=…from your server. allowed_originslimits framing. Only listed origins can embed the page. List the smallest set you need.- Create sessions from your server. Never ship your API key to a browser.
#Errors
| Status | Code | Cause |
|---|---|---|
| 400 | invalid_request | A field failed validation (non-https URL, bad accent, more than 5 origins, whabuzz.com in allowed_origins). message names it. |
| 400 | invalid_result_token | The result_token is forged, malformed or older than 10 minutes. |
| 401 | unauthorized | Missing or invalid key. |
| 403 | forbidden | The key is bound to one number, or lacks permission to manage. |
| 403 | es_disabled | Connecting numbers is switched off right now (complete). |
| 403 | owner_suspended | The owner account is suspended. Contact WhaBuzz support. |
| 404 | not_found | No such session on your account. |
| 409 | not_direct, invalid_state | complete on a hosted session, or a session that is connecting, waiting for verification, or over. |
The customer sees 404 link_invalid, 410 link_expired (not opened within 15 minutes) or 410 link_used (already opened) on the hosted page when the link is bad. Create a new session.
#Limits
| Limit | Value |
|---|---|
| Open the link within | 15 minutes |
| Browser binding | 24 hours |
| Connect attempts | 5 |
result_token lifetime | 10 minutes |
| Idle connected session completes after | 30 minutes |
allowed_origins | 5 |
return_url, cancel_url | 2048 characters |
List sessions with GET /v1/connect-sessions (newest first, limit and before). The same calls are in the Node SDK.
This page as Markdown: /onboard-a-number.md. All docs in one file: /llms-full.txt. Status.