# 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.

Canonical: https://docs.whabuzz.com/onboard-a-number/

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](https://docs.whabuzz.com/guides/connect-apps.md).

## Who does what {#rules}

- 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 `branding` the session carries.
- Each business verifies itself with Meta. WhaBuzz cannot do it for them. See `business_verification_status` in the [result](#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](https://docs.whabuzz.com/guides/tenants.md#caps) 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 {#quickstart}

### 1. Create a session {#create}

With a tenant API key (or an app key plus `X-WhaBuzz-Tenant`). A key bound to one number is refused with `403`.

```bash
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" }
  }'
```

```json
{
  "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](https://docs.whabuzz.com/guides/coexistence.md). |
| `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](#embedded)). Defaults to the origin of `return_url`. `whabuzz.com` is never allowed. |

### 2. Send the customer to the link {#redirect}

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:

1. Meta's Embedded Signup popup.
2. A phone re-verification step, only if Meta asks for one.
3. An offer to import templates from the business's other WhatsApp accounts, if you allowed it.
4. A payment-method step (the WhatsApp Manager billing page). See [billing_state](#result).
5. **Return to your product**.

### 3. Handle the return and verify it {#return}

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:

```bash
curl "https://api.whabuzz.com/v1/connect-sessions/cs_01JAB3N2R5T8W1Y4A7C0E3G6J9?result_token=1761128400.Xq…" \
  -H "Authorization: Bearer $WHABUZZ_KEY"
```

```json
{
  "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:

```js
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 {#webhook}

The return redirect only happens if the customer's browser makes it back. Subscribe an endpoint to `connect.completed` and `connect.failed` ([events](https://docs.whabuzz.com/events/whabuzz-events.md#connect)) 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 {#embedded}

To keep the customer on your page, open the link in a modal instead of redirecting:

```html
<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.js` falls back to a full-page redirect. The customer then returns through `return_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 {#direct}

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.

1. Create the session with `"mode": "direct"`. The answer carries the hosted `url` (keep it for the fallback) and an `embedded_signup` block:

```json
{
  "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
  }
}
```

2. Load Meta's JavaScript SDK before the customer clicks, then call `FB.login` inside the click (browsers block popups opened later):

```js
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.

3. Send the code and the two ids to your server, and from there:

```bash
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 `complete` again 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 {#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}

`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`](https://docs.whabuzz.com/guides/numbers.md) 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](https://docs.whabuzz.com/guides/connect-apps.md#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 {#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](https://docs.whabuzz.com/events/whabuzz-events.md#connect).

## Security {#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_token` is a signed value for one session and one final state, valid 10 minutes. Always call `GET /v1/connect-sessions/{id}?result_token=…` from your server.
- **`allowed_origins` limits 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 {#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 {#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`](https://docs.whabuzz.com/reference.md#tag/connect-sessions/GET/v1/connect-sessions) (newest first, `limit` and `before`). The same calls are in the [Node SDK](https://docs.whabuzz.com/sdks/node.md#connect-sessions).
