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.Three onboarding paths
When Embedded Signup is enabled,/welcome presents a chooser (OnboardingPathChooser) with three cards instead of jumping straight into the manual wizard:
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.
Feature flag and prerequisites
Embedded Signup is off by default and gated on both a public client flag and server-side app credentials:
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
- The browser loads the Facebook JS SDK once (
connect.facebook.net/en_US/sdk.js) and callsFB.login()with the resolvedconfig_id, requesting an authorizationcode. - Meta’s popup also posts a
WA_EMBEDDED_SIGNUPpostMessageevent (validated against afacebook.com/*.facebook.comorigin) carrying thewaba_idandphone_number_idthe operator selected inside Meta’s own UI. - Once both the
codeand the WABA/phone pair have arrived, the card POSTs once to/api/onboarding/meta/exchangewith{ code, wabaId, phoneNumberId, path, includeMarketingMessages }. - 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
debugTokenand requires bothwhatsapp_business_managementandwhatsapp_business_messagingscopes; - if Meta’s granular scopes enumerate granted WABA ids, the supplied
wabaIdmust be in that list — a client-supplied WABA id is never trusted blindly; - confirms the phone number actually belongs to that WABA via
listWabaPhoneNumbersbefore doing anything else; - generates and stores a one-time 6-digit registration PIN (
meta-phone-pinsecret) and callsregisterPhoneNumberForCloudApi— a Meta registration failure (already registered, 2FA mismatch on migrate) is tolerated and reported back asphoneRegistered: falserather 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.
- exchanges the code for a business access token (
- The response never contains the access token, the PIN, the verify token,
META_APP_SECRET, or anyappsecret_proof— onlychannelId,wabaId,phoneNumberId,displayName,phoneRegistered, andmarketingMessagesEnabled.
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-tokensecret kind — single-WABA workspaces are completely unaffected. - Each additional WABA claims the first free slot from
meta-access-token-2throughmeta-access-token-8(allocateWabaTokenSlotKind), recorded onwhatsapp_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-defaultmeta-access-tokenand then the envWHATSAPP_META_ACCESS_TOKENif the slot is empty. It never throws on a missing token — callers fail the dispatch loudly exactly as before.
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.
Verifying a multi-WABA workspace
- Check
whatsapp_provider_accountsfor the workspace: the default row hasis_default = trueandcredential_secret_kind = 'meta-access-token'; every additional WABA row has its owncredential_secret_kindslot andis_default = false. - A send for a non-default WABA that reports the wrong sender number almost always means its provider account’s
credential_secret_kindis missing or points at the wrong slot — checksetProviderAccountCredentialKindwas called during that WABA’s onboarding. - If Embedded Signup completed but
phoneRegisteredcame backfalse, 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 — the
/setupfirst-run flow and readiness checklist - Meta credentials — step-by-step setup — the manual bring-your-own-token path
- Connect WhatsApp — full Meta Business Suite walkthrough
- Vault Architecture — how workspace secrets (including WABA token slots) are stored and read