> ## Documentation Index
> Fetch the complete documentation index at: https://docs.switchbord.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Meta Embedded Signup & Multi-WABA

> How workspaces onboard additional WhatsApp Business Accounts through Meta Embedded Signup, and how Switchbord keeps each WABA's access token isolated at send time.

Switchbord workspaces are no longer limited to a single WhatsApp Business Account (WABA). A workspace can onboard additional WABAs through Meta's **Embedded Signup** flow — Meta's own hosted sign-in popup — without ever touching a manually-copied access token. Each WABA gets its own isolated credential slot so a second (or third) number never overwrites the first.

<Info>
  Embedded Signup is additive. The original bring-your-own-token wizard at `/welcome` is unchanged and always available — Embedded Signup is a second, optional path for workspaces that want Meta-hosted onboarding instead of pasting a System User token.
</Info>

## Three onboarding paths

When Embedded Signup is enabled, `/welcome` presents a chooser (`OnboardingPathChooser`) with three cards instead of jumping straight into the manual wizard:

| Path                                  | What it does                                                                                                                                        | Component                                      |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| **Bring your own token**              | The original manual wizard — paste a System User token, Phone Number ID, and WABA ID yourself. Always available, including self-hosted deployments. | `WelcomeWizard`                                |
| **Onboard with us**                   | Sign in with Meta and register a brand-new number with WhatsApp Business. No token copy/paste.                                                      | `EmbeddedSignupCard` (`defaultPath="onboard"`) |
| **Migrate existing WhatsApp account** | Sign in with Meta and move a number that's already registered with WhatsApp Business over to Switchbord.                                            | `EmbeddedSignupCard` (`defaultPath="migrate"`) |

Picking "Onboard with us" or "Migrate existing WhatsApp account" both render the same `EmbeddedSignupCard` — the only difference is the `path` value (`onboard` vs `migrate`) sent to the exchange endpoint, and Meta's own signup UI adapts its copy accordingly. A **Enable Marketing Messages** checkbox appears when a marketing-scoped Meta config ID is configured, and switches which Facebook login `config_id` is used.

<Tip>
  Every screen in the chooser has a "Choose a different method" back button, so an operator who starts Embedded Signup and hits a dead end (ad blocker, closed popup, Meta error) can always fall back to the manual token wizard without reloading the page.
</Tip>

## Feature flag and prerequisites

Embedded Signup is off by default and gated on **both** a public client flag and server-side app credentials:

| Requirement                                                              | Where                        | Effect when missing                                                                                                        |
| ------------------------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_EMBEDDED_SIGNUP_ENABLED=1` (or `true`/`on`/`enabled`/`yes`) | Vercel `apps/app` env        | `/welcome` renders only the manual wizard — the chooser never appears                                                      |
| `NEXT_PUBLIC_META_APP_ID` + `NEXT_PUBLIC_META_CONFIG_ID`                 | Vercel `apps/app` env        | Client flag resolves to disabled even if the env var above is set (the Facebook JS SDK can't run without them)             |
| `NEXT_PUBLIC_META_CONFIG_ID_MM` (optional)                               | Vercel `apps/app` env        | The "Enable Marketing Messages" checkbox is hidden; every signup uses the standard config                                  |
| `META_APP_ID` + `META_APP_SECRET`                                        | Vercel `apps/app` server env | The exchange API route (`/api/onboarding/meta/exchange`) responds `404 embedded_signup_disabled` even if the UI is visible |

The server-side check is deliberate defense in depth: flipping the client flag alone can never expose a working Embedded Signup flow without the app credentials also being configured.

## How the exchange works

1. The browser loads the Facebook JS SDK once (`connect.facebook.net/en_US/sdk.js`) and calls `FB.login()` with the resolved `config_id`, requesting an authorization `code`.
2. Meta's popup also posts a `WA_EMBEDDED_SIGNUP` `postMessage` event (validated against a `facebook.com` / `*.facebook.com` origin) carrying the `waba_id` and `phone_number_id` the operator selected inside Meta's own UI.
3. Once **both** the `code` and the WABA/phone pair have arrived, the card POSTs once to `/api/onboarding/meta/exchange` with `{ code, wabaId, phoneNumberId, path, includeMarketingMessages }`.
4. The route (`apps/app/app/api/onboarding/meta/exchange/route.ts`) runs a strict server-side sequence:
   * exchanges the code for a business access token (`exchangeEmbeddedSignupCode`);
   * inspects the token with `debugToken` and requires both `whatsapp_business_management` and `whatsapp_business_messaging` scopes;
   * if Meta's granular scopes enumerate granted WABA ids, the supplied `wabaId` **must** be in that list — a client-supplied WABA id is never trusted blindly;
   * confirms the phone number actually belongs to that WABA via `listWabaPhoneNumbers` before doing anything else;
   * generates and stores a one-time 6-digit registration PIN (`meta-phone-pin` secret) and calls `registerPhoneNumberForCloudApi` — a Meta registration failure (already registered, 2FA mismatch on migrate) is tolerated and reported back as `phoneRegistered: false` rather than failing the whole request;
   * generates a per-workspace verify token and subscribes the Switchbord app to the WABA with a workspace-scoped `override_callback_uri` (`.../webhooks/meta?w=<workspaceId>`);
   * resolves which credential slot this WABA's token belongs in (see below), stores the token, and imports the phone number into a channel.
5. The response never contains the access token, the PIN, the verify token, `META_APP_SECRET`, or any `appsecret_proof` — only `channelId`, `wabaId`, `phoneNumberId`, `displayName`, `phoneRegistered`, and `marketingMessagesEnabled`.

<Warning>
  The exchange route enforces a strict secret-leak invariant: `detail` fields on error responses may only ever carry Meta's own sanitized error message. Tokens, PINs, verify tokens, and the app secret are never logged, captured in exception telemetry, or echoed back to the browser.
</Warning>

## Multi-WABA token isolation

Before this work, every workspace's Meta token lived under one Vault secret kind (`meta-access-token`) — a second WABA connected via Embedded Signup would silently overwrite the first WABA's token and break its sends. Switchbord now resolves each WABA's token independently:

* The **default** provider account (the first WABA in a workspace, or one already flagged default) keeps using the `meta-access-token` secret kind — single-WABA workspaces are completely unaffected.
* Each **additional** WABA claims the first free slot from `meta-access-token-2` through `meta-access-token-8` (`allocateWabaTokenSlotKind`), recorded on `whatsapp_provider_accounts.credential_secret_kind`. Eight slots comfortably exceeds any realistic per-workspace WABA count.
* `resolveChannelAccessToken(admin, { workspaceId, channelId | channel })` is the single resolver used by every send hot path (message dispatch — which also covers campaign sends — media sends, interactive sends, and template sync). It resolves channel → provider account → `credentialSecretKind`, reads that Vault slot, and falls back to the workspace-default `meta-access-token` and then the env `WHATSAPP_META_ACCESS_TOKEN` if the slot is empty. It never throws on a missing token — callers fail the dispatch loudly exactly as before.

```text theme={null}
resolveChannelAccessToken
  1. channel → provider account (whatsapp_provider_accounts)
  2. provider account → credential_secret_kind ("meta-access-token" | "meta-access-token-2".."-8")
  3. readWorkspaceSecret({ workspaceId, kind: credential_secret_kind })
       ↳ if empty and kind ≠ default → retry with "meta-access-token"
       ↳ if still empty → env WHATSAPP_META_ACCESS_TOKEN
```

<Info>
  This is transparent to operators — there is no per-WABA token field to manage in Settings. The slot kind is chosen automatically at onboarding time and read automatically at send time. Settings → Provider continues to show only the operator-facing credential kinds; the WABA slot kinds are internal.
</Info>

## Verifying a multi-WABA workspace

* Check `whatsapp_provider_accounts` for the workspace: the default row has `is_default = true` and `credential_secret_kind = 'meta-access-token'`; every additional WABA row has its own `credential_secret_kind` slot and `is_default = false`.
* A send for a non-default WABA that reports the wrong sender number almost always means its provider account's `credential_secret_kind` is missing or points at the wrong slot — check `setProviderAccountCredentialKind` was called during that WABA's onboarding.
* If Embedded Signup completed but `phoneRegistered` came back `false`, the phone number connected to the channel but is not yet registered for Cloud API sends — finish registration from Settings before expecting sends to go out.

## See also

* [Operator Onboarding](/platform/onboarding) — the `/setup` first-run flow and readiness checklist
* [Meta credentials — step-by-step setup](/getting-started/meta-credentials) — the manual bring-your-own-token path
* [Connect WhatsApp](/platform/connect-whatsapp) — full Meta Business Suite walkthrough
* [Vault Architecture](/security/vault-architecture) — how workspace secrets (including WABA token slots) are stored and read
