WhatsApp Embedded Signup Guide

The vendor-neutral implementer's guide to Meta's Embedded Signup flow: prerequisites, the exact client and server calls, Coexistence, v4, and what breaks in production.

Looking for the high-level overview first? Start with WhatsApp Business API and then return here for the implementation details.

This is the vendor-neutral implementer's guide to Meta's WhatsApp Embedded Signup: what the flow is, what you must configure before it will run, the exact client and server calls, and the parts that break in production. For how Dualhook itself drives the flow, see Embedded Signup.

Verified against Meta's WhatsApp Business Platform documentation on 14 August 2026.

Direct Answer

WhatsApp Embedded Signup is Meta's OAuth popup flow that lets a customer connect their own WhatsApp Business Account and phone number to your app in a few clicks, without ever handling an access token themselves.

You launch it from your own UI with the Facebook JavaScript SDK. The customer logs into Meta, picks or creates a Meta Business Portfolio, a WhatsApp Business Account, and a phone number, and approves your app's access. Meta returns a short-lived authorization code to your page and, separately, a postMessage carrying the waba_id and usually the phone_number_id. Your server exchanges that code for a customer-scoped Business Integration System User (BISU) access token, subscribes your app to the WABA, and configures webhooks.

Embedded Signup is the only supported way for a Tech Provider, Tech Partner, or Solution Partner to onboard a customer's WhatsApp assets at scale. There is no supported manual alternative for third-party onboarding: token copying by hand is a self-serve, single-business pattern, not an onboarding product.

What Embedded Signup Actually Is

Strip away the marketing and Embedded Signup is three things bolted together:

  1. A Facebook Login for Business configuration that declares which assets you want (WhatsApp Business Accounts and phone numbers), which permissions you want (whatsapp_business_management, whatsapp_business_messaging), and what kind of token comes out.
  2. A hosted multi-step wizard that Meta renders inside a popup. Business portfolio selection, WABA selection or creation, phone number entry, display name, verification code, and — in the Coexistence variation — the "connect your existing WhatsApp Business app account" branch.
  3. Two independent return channels. The OAuth authorization code comes back through the FB.login callback. The IDs of what the customer actually selected come back through a window.postMessage from facebook.com. These are separate and can arrive out of order, or one without the other. Nearly every hard onboarding bug in this flow traces back to treating them as one channel.

The resulting token is a Business Integration System User token: it belongs to the customer's business, is scoped to the assets they granted, and is issued to your app. It is not your app token, and it is not a user token. It should never be shown to the customer or embedded in a browser.

Prerequisites

Before the popup will open at all:

RequirementWhereNotes
Meta app with the WhatsApp product addedMeta App DashboardBusiness-type app
Facebook Login for Business v4 configurationApp Dashboard, Embedded Signup BuilderThe config_id you pass to FB.login
whatsapp_business_managementApp permissionsManage WABAs, templates, phone numbers
whatsapp_business_messagingApp permissionsSend and receive messages
Advanced Access for both scopes plus public_profileApp ReviewStandard Access only works for people with a role on your app
Verified Meta Business PortfolioMeta Business SettingsYour own portfolio, not the customer's
HTTPS domain in Allowed DomainsFacebook Login settingsExact host, no wildcards under Strict Mode
Exact HTTPS callback in Valid OAuth Redirect URIsFacebook Login settingsMust match byte for byte
A live webhook endpointYour infrastructureMust answer Meta's verification challenge before you can route anything

Two of these fail quietly and cost people days:

  • Advanced Access. With Standard Access the popup runs fine for you and fails for every real customer. Test with an account that has no role on your app.
  • Strict Mode redirect URIs. Meta compares the redirect URI literally, including trailing slashes and query strings.

The Flow, End to End

  1. Your customer clicks Connect WhatsApp in your product.
  2. You collect anything you need up front — for example the customer's own webhook URL and verify token, if you intend to route webhooks directly to them.
  3. You call FB.login with your config_id and response_type: "code". Meta opens the popup.
  4. The customer authenticates, picks a business portfolio, picks or creates a WABA, and adds or selects a phone number.
  5. Meta posts a WA_EMBEDDED_SIGNUP message to your page containing waba_id and, in most flows, phone_number_id.
  6. The FB.login callback fires with authResponse.code.
  7. Your server exchanges the code for a BISU token, subscribes your app to the WABA, configures webhooks, and registers the number.

Steps 5 and 6 race. Handle that explicitly — see Step 4.

Step 1: Create the Login for Business v4 Configuration

In the Meta App Dashboard, open the Embedded Signup Builder and create a Facebook Login for Business v4 configuration with the WhatsApp Embedded Signup variation.

Configure it with:

  • the Cloud API product selected
  • WhatsApp Business Accounts and WhatsApp phone numbers as the selected assets
  • a Business Integration System User token with never-expiring access
  • exactly two permissions: whatsapp_business_management and whatsapp_business_messaging
  • nothing else selected — no business_management, no ad accounts, no Pages, no Instagram, no datasets, no catalogs, no Marketing Messages Lite

Requesting scopes you do not need is the most common reason an App Review submission stalls, and every extra asset type you select adds a consent screen your customer has to read.

Under Facebook Login → Settings, enable Client OAuth Login, Web OAuth Login, Enforce HTTPS, Embedded Browser OAuth Login, Strict Mode for redirect URIs, and Login with the JavaScript SDK.

The configuration produces a configuration ID. That is the config_id your client code passes. Treat it as environment configuration, not a constant: you will eventually need to swap it, and you want that to be a deploy, not a code change.

Step 2: Load the Facebook JavaScript SDK

<script async defer crossorigin="anonymous"
        src="https://connect.facebook.net/en_US/sdk.js"></script>
window.fbAsyncInit = function () {
  FB.init({
    appId: process.env.NEXT_PUBLIC_META_APP_ID,
    autoLogAppEvents: true,
    xfbml: true,
    version: "v25.0",
  });
};

The SDK must be loaded before the customer can click your button. If window.FB is undefined when they click, fail loudly with a "refresh and try again" message rather than silently doing nothing — a dead button is indistinguishable from a broken product.

Step 3: Launch the Popup with FB.login

FB.login(
  (response) => {
    if (response.authResponse?.code) {
      // Send the code to your server. Never exchange it in the browser:
      // the exchange requires your app secret.
      void completeSignup(response.authResponse.code);
      return;
    }
    // status === "unknown" covers cancellation, popup blocking, and
    // documented Embedded Signup errors. Classify with the postMessage.
    handleAbandonedSignup();
  },
  {
    config_id: process.env.NEXT_PUBLIC_META_CONFIG_ID,
    response_type: "code",
    override_default_response_type: true,
    extras: {
      setup: {},
      // Coexistence only. Omit featureType for a plain Cloud API flow.
      featureType: "whatsapp_business_app_onboarding",
    },
  }
);

What each field does:

FieldPurpose
config_idThe Login for Business v4 configuration from Step 1
response_type: "code"Returns an authorization code instead of a browser access token
override_default_response_type: trueRequired, or the SDK falls back to its default token response
extras.setupPrefill object. An empty object means "ask the customer for everything"
extras.featureTypeLaunch selector. whatsapp_business_app_onboarding offers the Coexistence branch

Under v4, the Builder configuration — not the JavaScript call — controls products, assets, and permissions. The generic feature: "whatsapp_embedded_signup" override belongs to older versions and does nothing here.

sessionInfoVersion is a more nuanced case. Meta's Coexistence page still shows "sessionInfoVersion": "3" in its sample extras object, but the v4 page makes no mention of it, and a v4 configuration works without it — Dualhook's own production integration omits it and still receives the Coexistence completion payload, which reports version: 3 regardless. Treat it as vestigial rather than required, and if you are debugging an unexplained flow difference, adding it back is a cheap thing to test.

The one thing v4 genuinely does not move into the Builder is the launch selector. Meta's Version 4 page is explicit that "onboarding WhatsApp Business app users continues to be supported through the feature_type parameter," so featureType: "whatsapp_business_app_onboarding" still has to be passed in extras for the Coexistence screen to appear, even on a v4 configuration that has Coexistence enabled. A bare extras: { setup: {} } runs a clean Cloud API onboarding and quietly hides the Coexistence option — which is the correct call if you do not support Coexistence, and a silent bug if you do.

FB.login must be called synchronously inside the click handler. If you await anything first, the browser treats the popup as unsolicited and blocks it.

Step 4: Capture the Session Info Message

Meta sends the selected asset IDs through postMessage, not through the login callback.

const listener = (event) => {
  if (
    event.origin !== "https://www.facebook.com" &&
    event.origin !== "https://web.facebook.com"
  ) {
    return;
  }

  let data;
  try {
    data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
  } catch {
    return;
  }

  if (data.type !== "WA_EMBEDDED_SIGNUP") return;

  const event_name = String(data.event ?? "").toUpperCase();

  if (data.data?.waba_id) {
    sessionInfo = {
      waba_id: data.data.waba_id,
      phone_number_id: data.data.phone_number_id ?? null,
    };
  }

  if (event_name === "FINISH" ||
      event_name === "FINISH_ONLY_WABA" ||
      event_name === "FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING") {
    // Meta linked the account. The code may still be in flight.
  } else if (event_name === "CANCEL") {
    // data.data.current_step tells you where they dropped out.
  } else if (event_name === "ERROR") {
    // data.data.error_id / error_message / session_id
  }
};

window.addEventListener("message", listener);

Five rules that separate a reliable integration from a flaky one:

  1. Always check event.origin. Any page can post a message to yours. Accept only https://www.facebook.com and https://web.facebook.com.
  2. Expect phone_number_id to be missing. The Coexistence completion event routinely omits it. Fall back to server-side discovery rather than failing the connection.
  3. Do not treat FINISH as success. FINISH means Meta linked the account. It does not mean the authorization code reached your server. Those are different outcomes with different recoveries.
  4. Do not treat PARTNER_ADDED or PARTNER_APP_INSTALLED as completion. They fire mid-flow. They are corroborating evidence, not proof.
  5. Give the message a grace period. When the callback fires first, poll for the session info for a few seconds before giving up on it.

Do not log the raw event. It can carry localized error text and selected-business payloads you have no reason to retain. Store the numeric error ID, Meta's session ID, and the failed step, and nothing else.

Step 5: Exchange the Code for a Business Token

Server-side only. This call needs your app secret.

curl -X GET "https://graph.facebook.com/v25.0/oauth/access_token" \
  -d "client_id=<APP_ID>" \
  -d "client_secret=<APP_SECRET>" \
  -d "code=<AUTH_CODE>"

You get back a Business Integration System User access token scoped to the assets the customer granted. Under a correctly built v4 configuration it does not expire.

Immediately after the exchange, call debug_token to find out what you actually received:

curl -X GET "https://graph.facebook.com/v25.0/debug_token?input_token=<BISU_TOKEN>" \
  -H "Authorization: Bearer <APP_ACCESS_TOKEN>"

The response's granular_scopes tells you which WABA target_ids carry whatsapp_business_management and whatsapp_business_messaging. This is your ground truth when the postMessage never arrived, and it is the first thing to check when sends later fail with error 200 or error 10.

Handling rules:

  • Store the token encrypted at rest. Never return it to the browser, never show it in your UI, never put it in a log line.
  • Record issued_at and expires_at so you can warn before a non-permanent token dies. A BISU token is not refreshable through the ordinary System User refresh flow; recovery means the customer re-runs Embedded Signup.
  • Once you hold a token, every subsequent Graph call should be sent with appsecret_proof. Without it, a leaked token is usable by anyone who has it.
  • The code exchange itself takes no appsecret_proof — there is no token yet.

Authorization codes are single-use and short-lived. If your server retries the exchange after a network timeout, the second attempt fails; treat it as terminal and have the customer restart.

Step 6: Subscribe Your App and Route Webhooks

A token alone delivers nothing. Two more calls:

# 1. Subscribe your app to the customer's WABA
curl -X POST "https://graph.facebook.com/v25.0/<WABA_ID>/subscribed_apps" \
  -H "Authorization: Bearer <BISU_TOKEN>"
# 2. Optional: override the callback so Meta delivers straight to the customer
curl -X POST "https://graph.facebook.com/v25.0/<WABA_ID>/subscribed_apps" \
  -H "Authorization: Bearer <BISU_TOKEN>" \
  -d "override_callback_uri=https://customer.example.com/webhooks/whatsapp" \
  -d "verify_token=<CUSTOMER_VERIFY_TOKEN>"

Order matters. Overriding before subscribing returns Graph error 100 with "Before override the current callback uri" — see Webhook Error 100.

Whichever endpoint receives the traffic must already answer Meta's verification challenge. Meta sends a GET with hub.mode=subscribe, hub.verify_token, and hub.challenge. Compare the verify token, then return the raw hub.challenge value as plain text with HTTP 200 — not JSON, not quoted, not wrapped.

app.get("/webhooks/whatsapp", (req, res) => {
  const mode = req.query["hub.mode"];
  const token = req.query["hub.verify_token"];
  const challenge = req.query["hub.challenge"];

  if (mode === "subscribe" && token === process.env.VERIFY_TOKEN) {
    return res.status(200).send(challenge);
  }
  return res.sendStatus(403);
});

Set this endpoint up before you run Embedded Signup, not after. Meta verifies at configuration time, and a 404 at that moment turns a clean onboarding into a support ticket.

Webhook Override is what makes direct routing possible: message-path fields go from Meta to the customer's own server, while management events stay on your app-level callback. See the webhook subscription field reference for which fields are overridable.

Step 7: Register the Phone Number

A number selected in the popup is not yet usable by Cloud API. Register it:

curl -X POST "https://graph.facebook.com/v25.0/<PHONE_NUMBER_ID>/register" \
  -H "Authorization: Bearer <BISU_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"messaging_product":"whatsapp","pin":"<SIX_DIGIT_PIN>"}'

The pin is the number's two-step verification PIN. If two-step verification was never set, you are setting it here. If it was set and you do not know it, registration fails and the customer must reset it in WhatsApp Manager.

Two failures are common enough to name:

  • Subcode 2388001 — Meta does not publicly document this subcode. It is commonly seen when a number is still attached to WhatsApp, the WhatsApp Business app, or a previous provider, but confirm the actual ownership state before changing anything. Use Coexistence if the business must keep the app, and do not delete or deregister an account as a generic fix.
  • Error 133016 — too many registration attempts. Retrying faster makes the cooldown longer.

Coexistence: Onboarding WhatsApp Business App Users

Most businesses that want the API already use the WhatsApp Business app, and most of them do not want to give it up. Coexistence is Meta's supported answer: the app and Cloud API run on the same number.

Onboarding differences you have to plan for:

  • Pass featureType: "whatsapp_business_app_onboarding" in extras. Without it, the customer never sees the "connect your existing WhatsApp Business app account" screen.
  • The completion event is FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING, and its payload can omit phone_number_id. Discover the number server-side from the WABA.
  • The customer completes a QR-code or in-app confirmation step on the phone itself. That is outside your UI — build for the wait.
  • After the override is accepted, request the one-time contact sync (smb_app_state_sync) and then chat-history sync (history). Both are one-shot and time-sensitive. If one request fails, still attempt the other.
  • Coexistence adds an ongoing operational requirement: the app must be opened at least once every 13 days. See Heartbeat.
  • Messages sent from the app arrive as smb_message_echoes, not as messages.

If the number was previously onboarded by another provider, Embedded Signup can fail before it grants you anything with #2655093 — see Error 2655093.

Embedded Signup Versions: v2, v3, and v4

VersionHow it is configuredStatus
v2Assets and permissions declared in the FB.login callLegacy. Meta has set 15 October 2026 as its end of life
v3Required extras.version: "v3" to opt inTransitional
v4Assets, permissions, and products declared in the Embedded Signup Builder configurationCurrent

A detail that catches teams out during the audit: if you never explicitly passed extras.version: "v3", you were on v2 regardless of when you built your integration. Opting into v3 was never automatic, so "we built this recently" is not evidence of anything. Check the actual extras object you send.

Migrating to v4 is not only a code change. You need a new configuration built in the Builder with the products and assets selected, plus the code change that stops sending v2-style launch parameters. Ship both together, and validate the new configuration against both a plain Cloud API onboarding and a Coexistence onboarding — including the chat-history opt-in branch — before pointing production at it. A configuration that works for one and silently breaks the other is the normal failure, not the exotic one.

Embedded Signup vs Manual Onboarding

Embedded SignupManual setup
Who does the workThe customer, in a popupYour engineers, per customer
Token handlingServer-side code exchangeCustomer copies a token into your UI
Asset selectionMeta's wizardManual ID collection
Number registrationPart of the flowA separate Graph call you script
Webhook setupConfigurable during onboardingManual per WABA
Coexistence supportYes, via the launch selectorNot available
Scales to many customersYesNo
Supported by Meta for third-party onboardingYesNo

Manual token entry remains reasonable for exactly one case: a single business wiring up its own number for its own backend. The moment there is a second business, you are building an onboarding product, and Embedded Signup is the supported shape.

What Goes Wrong, and How to Read It

SymptomLikely causeWhere to look
Popup never opensFB.login called after an await, or SDK not loadedCall it synchronously in the click handler
Works for you, fails for customersStandard Access instead of Advanced AccessApp Review
status: "unknown", no codePopup blocked, cancelled, or a documented Meta errorClassify using the postMessage
FINISH received, no connection createdThe code did not return to your serverConnection Not Appearing
Code exchange returns an OAuth errorRedirect URI mismatch, reused code, or wrong app secretStrict Mode redirect URIs
#2655093 in the popupThe number is still shared with a previous partnerError 2655093
Webhook override rejected with code 100Override attempted before subscribingWebhook Error 100
Registration fails with subcode 2388001Number still attached to WhatsApp, the Business app, or a previous providerConfirm the ownership state; use Coexistence if the app must stay. Do not delete an account as a generic fix
Sends fail with 200 or 10 after onboardingToken lacks the scope for that WABAdebug_token granular scopes

The full index is at Embedded Signup Errors; numeric Cloud API codes are at WhatsApp Cloud API Error Codes.

Implementation Checklist

  • Meta app with the WhatsApp product, business type
  • Login for Business v4 configuration built in the Embedded Signup Builder
  • Advanced Access approved for whatsapp_business_management, whatsapp_business_messaging, and public_profile
  • Allowed Domains and Valid OAuth Redirect URIs set exactly, with Strict Mode on
  • Webhook endpoint live and answering hub.challenge before you run the flow
  • FB.login called synchronously with config_id, response_type: "code", and override_default_response_type: true
  • featureType: "whatsapp_business_app_onboarding" in extras if you support Coexistence
  • Origin-checked postMessage listener that tolerates a missing phone_number_id
  • Server-side code exchange, then debug_token to record granular scopes
  • Token encrypted at rest, never returned to the browser, appsecret_proof on every later call
  • subscribed_apps before any override_callback_uri
  • Phone number registered with a known two-step PIN
  • A recovery path for FINISH without a resulting connection
  • Tested end to end with an account that has no role on your app

Where Dualhook Fits

Dualhook is a badged Meta Tech Partner and runs this flow as a product. The Meta app, the v4 configuration, App Review, the code exchange, token encryption, debug_token scope capture, subscribed_apps, Webhook Override, and the recovery path for a FINISH that never returned an authorization are all handled for you.

The part worth knowing about is where the traffic goes afterwards. Dualhook asks for your webhook URL and verify token before the popup opens and configures a WABA-level override at completion, so message-path webhooks go from Meta straight to your endpoint. Dualhook is not in the message path and does not store message content. See Webhook Override and WhatsApp API Privacy.

If you are building the flow yourself, the checklist above is the whole job. If you would rather not own App Review, configuration migrations, and popup-error triage, that is what Dualhook is for.

FAQ

What is WhatsApp Embedded Signup?

Meta's OAuth-based onboarding flow. It lets a customer connect their own WhatsApp Business Account and phone number to a third-party app through a popup. The customer authorizes access, Meta returns a short-lived authorization code, and the integrator exchanges it server-side for a Business Integration System User access token. The customer never handles the token.

Who can use Embedded Signup?

Any Meta partner that has added the WhatsApp product to a business-type app and holds Advanced Access for whatsapp_business_management and whatsapp_business_messaging — Tech Providers, Tech Partners, and Solution Partners. It is the supported path for onboarding customer-owned WhatsApp assets at scale.

What do I need before I can run the flow?

A Meta app with the WhatsApp product, a Facebook Login for Business v4 configuration created in the Embedded Signup Builder, Advanced Access for the two WhatsApp scopes plus public_profile, exact HTTPS entries in Allowed Domains and Valid OAuth Redirect URIs, and a live webhook endpoint that answers Meta's hub.challenge verification.

How does the token exchange work?

Meta returns a short-lived authorization code to your page through the FB.login callback. You send it to your server, which calls GET /v25.0/oauth/access_token with your app ID, app secret, and the code. The response is a Business Integration System User token scoped to the assets the customer granted. Store it encrypted; never return it to the browser.

Why is phone_number_id missing from the session info?

Because Meta does not always include it, particularly in the Coexistence completion event FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING. Fall back to server-side discovery: call debug_token, read the WABA target_ids from granular_scopes, and enumerate the phone numbers under each WABA.

Does a FINISH event mean the signup succeeded?

No. FINISH means Meta linked the account. The authorization code travels on a separate channel and can still fail to reach your server. Treat "Meta linked it but we never got the code" as its own state with its own recovery, rather than as either success or failure.

What is the difference between Embedded Signup v2, v3, and v4?

v2 declared assets and permissions in the FB.login call. v3 required an explicit extras.version: "v3" opt-in. v4 moves products, assets, and permissions into the Embedded Signup Builder configuration, so the JavaScript call carries only config_id, the response type, and the launch selector. Meta has set 15 October 2026 as the end of life for v2.

How do I onboard a business that already uses the WhatsApp Business app?

Pass featureType: "whatsapp_business_app_onboarding" in extras. That launch selector is what surfaces the Coexistence branch, and it is still required under v4 even when the Builder configuration has Coexistence enabled. Expect the completion event FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING, and expect it to sometimes omit the phone number ID.

Can I exchange the authorization code in the browser?

No. The exchange requires your app secret, which must never reach a browser. Post the code to your own server and exchange it there. Codes are single-use and short-lived, so a retried exchange after a timeout will fail.

Sources

All checked 14 August 2026. Meta is the authority for every field name, scope, and date above; re-verify before you commit a migration date to a plan.

Related

  • Embedded SignupHow Dualhook uses Meta's Embedded Signup flow to connect WhatsApp Business Accounts via OAuth.
  • Embedded Signup ErrorsSearchable Meta Embedded Signup error index with safe first actions for business, WABA, phone, OTP, sharing, and account restrictions.
  • Connection Not Appearing After Embedded SignupWhy Meta can finish linking while no Dualhook connection appears, and how to retry the browser handoff safely.
  • WhatsApp Webhook OverrideHow Dualhook uses WhatsApp Webhook Override to route supported customer-path webhooks directly from Meta to your server.
  • WhatsApp CoexistenceHow Coexistence mode works: using WhatsApp Business App and Cloud API on the same number.
Browse more docsStart Free Trial