Embedded Signup in your app
The recommended way to run Meta Embedded Signup inside your product is to embed Pilot Status’s hosted Connect button in an iframe. Your customer clicks Continue with Facebook, completes Meta’s popup, and their official WhatsApp Cloud API number lands in your Pilot Status account. The Facebook SDK and the login popup live inside the hosted page onconnect.pilotstatus.com.br. Your app loads no Facebook SDK, runs no Facebook login code, and never touches Meta’s APIs — none of the Meta wiring is yours to set up. It does exactly two things:
- Your backend generates the connection link (
POST /v1/numbers/remote-pairingwith your tenantps_key). - Your page embeds the returned link in an iframe and listens for the
connect:pairedmessage.
Prefer a running start? Download the MIT-licensed demo — a Node backend and a React frontend wired to exactly this flow, with ready-made button presets: embedded-signup-demo.zip. Unzip, set your
ps_ key, npm run dev (or docker compose up).This page is the step-by-step: follow the four steps in order and you have a working integration. Its sibling, Embed the Connect Page, is the reference — every option, both providers (Cloud API and QR), the full postMessage protocol and the complete
branding.button field table. This page links there instead of restating it, so you always read one copy.Architecture
What you need
One credential: a tenant-scoped API key. Copy it from the dashboard at/profile, under the API tab. It starts with ps_.
It must be tenant-scoped, not number-scoped: generating a link creates a brand-new placeholder number, so a key bound to one existing number cannot authorize it.
There is nothing else to provision. You do not register a Meta app, submit App Review, or create a number-scoped key — there is no Meta app wiring of any kind on your side. Everything the popup needs already lives on the hosted page. If your key was issued through OAuth with per-number consent grants, the route returns
403 NUMBERS_GRANT_NOT_ALLOWED — use a tenant key instead.ps_ key on your server. The browser never sees it — it only embeds the iframe.
The flow
1
Your backend generates the connection link
Call The response is 201 with exactly three fields:Hand The placeholder’s id is not in the response body. It lives in the
POST /v1/numbers/remote-pairing with provider: "META" and metaFlow: "embedded", using your tenant key. Pass branding.button to style the button that renders inside the iframe.cURL
remotePairingUrl down to the page you’ll embed it on — that is the connectUrl in the diagram above. externalRef (your own customer id) is signed into the link and echoed back to you on connect:paired, so you can join the result to your records.This is the part worth being precise about, because the two halves live in different places:- This call decides the style and the shape.
branding.buttonis signed into the token, and sending it is also what makes the link render the bare button instead of the full page. One object, both effects. - The
?embed=1&parentOrigin=…parameters in step 2 decide the plumbing. They are what turn on thepostMessagechannel back to your page. Without them the button still renders and still works — you just never hear about the result.
branding.button accepts. All ten fields are optional, and the full table — values, defaults, ranges, plus hideProvenance — is in the reference: Button styling. The three that trip people up:iconfollowsvariantwhen you omit it.facebook→ Facebook glyph,whatsapp→ WhatsApp glyph, any other variant → no glyph at all. Pass"none"to drop it deliberately.widthoverridesfullWidth. Send a width andfullWidthis ignored, whatever you set it to.- A bad value is rejected, not corrected. Out-of-range or off-enum gets you
400 Validation errorwith adetailsobject — and no link, no placeholder, no slot consumed.height: 1000does not fall back to the default; it fails the whole call.
branding.button can only be set here, when you mint the link — never through a query param on the iframe URL. That is deliberate: a stranger who sees the iframe URL cannot restyle your button, and every styled page stays attributable to a tenant.numberId claim of the token — the last path segment of remotePairingUrl. Decode the base64url payload to read it (a metadata read; no signature check needed) and store it next to your customer record, because that id is what you need for the delete-on-abandon cleanup in step 3, and it lets you register the webhook (step 4) before the customer finishes. The same id arrives again on connect:paired. Those two — the decoded claim and the event — are the supported ways to obtain it.Your backend
The link carries a signed token with a 30-minute TTL. In the hosted model this token rides in the iframe URL — that is inherent to embedding a hosted page, so generate the link just before the customer clicks connect, not hours ahead.
2
Embed the hosted button in an iframe
Append
?embed=1&mode=button&parentOrigin=<your page origin> to the link and drop it in an iframe. parentOrigin is your page’s location.origin — only the browser knows it, so build the src in the browser.The Facebook SDK and the login popup run inside this iframe, on
connect.pilotstatus.com.br — a domain already registered with Pilot Status’s Meta app. Your app loads no Facebook SDK, runs no Facebook login code, and registers no domain of its own: none of the Meta app wiring is yours to provision.Button mode renders only the button, and clears the page background — that is a property of
mode=button itself, so whatever sits behind the frame shows through. You don’t have to ask for it.The small “secured by pilotstatus.com.br” marker is a separate, independent rule: it is hidden by default when the page really is inside an iframe, and it does show when the same link is opened top-level, on purpose — a stand-alone styled login button on a *.pilotstatus.com.br URL with no visible provenance is exactly what Safe Browsing flags as deceptive.3
Listen for connect:paired
The iframe posts messages to your page. Validate On See Clean up an abandoned link below for the delete call in full.
event.origin on every one — it must equal https://connect.pilotstatus.com.br.connect:paired you receive { numberId, phone, displayName, provider, externalRef, redirectUrl }. Every field is always present — a flow that cannot know one sends null rather than omitting it, so you never need per-path branching.numberIdis the id you use everywhere else in the API (GET /v1/numbers/{id}, webhooks, sending). Persist it.externalRefis whatever you passed when you generated the link — match it against your own customer record.redirectUrl, when set, is handed to you rather than followed — your page decides whether and where to navigate.
connect:expired (generate a new link), connect:error (surface the message), and resize (adjust the iframe height, which starts small and grows once the button renders).Cancel, retry, and the plan slot
connect:error arrives on a terminal failure and when the customer cancels or closes the Facebook popup — Facebook does not tell the two apart, so both reach you the same way, with the same { message } payload. The button stays on screen either way, so the person can simply click again.That matters for cost: the placeholder number created in step 1 is already holding a plan slot, and nothing frees it on its own. So run a delete-on-abandon cleanup whenever the session ends without a connect:paired — you closed the modal, the customer navigated away, or you gave up — and not on the first connect:error, which they can still retry past. The browser tells your backend the session died (the browser has no ps_ key), and your backend deletes the placeholder with DELETE /v1/numbers/{id}, returning the slot right away instead of leaving it parked.Where the id comes from: it is the numberId you decoded from the token in step 1 and stored alongside your customer record — that is what your page sends to your own backend here. (If the flow got as far as connect:paired, the same id arrived in that payload.) Hand the browser that id when you hand it the connectUrl, and the cleanup needs no lookup at all.Browser
Your backend
4
Register a webhook for the connected number
With the Delivery is queued and retried 5 times with exponential backoff starting at 5 seconds. Return
numberId in hand — decoded from the token in step 1, or received on connect:paired — subscribe your endpoint with POST /v1/webhooks, using the same tenant key. Since the id is already final at link-generation time, you can register the webhook before the customer finishes and not miss the first inbound messages. See Configure webhooks for the full CRUD surface.cURL
2xx quickly and process asynchronously.Clean up an abandoned link
If a customer never finishes, the placeholder number keeps holding its plan slot. Remove it withDELETE /v1/numbers/{id}, using your tenant key. The id is the numberId you decoded from the token when you generated the link (step 1), or the one delivered on connect:paired — a number id or an instance id both work. This is the call behind the delete-on-abandon pattern in step 3.
cURL
404 with "Number not found". You can also delete orphaned placeholders from the dashboard at /numbers.
Deleting the placeholder frees the slot, but it does not invalidate the link: the token is verified on signature, kind, and expiry alone. Until it expires, a customer who retries will still complete — and, with the placeholder gone, a new number is created and a new slot consumed. So delete once you are confident nobody is coming back, or be ready for a second, unexpected number.
Prefer your own markup?
If you have a fixed domain and want the button to render on your own page, you can run Meta Embedded Signup yourself by loading the Facebook JavaScript SDK and drivingFB.login in the browser — but that means registering your own Meta app config, wiring the popup’s two message channels, and owning the CORS setup. For almost everyone, the hosted button in this guide is the recommended path: zero SDK, zero Meta app, zero config.
AI-builder prompts
Copy-paste prompts that scaffold this exact hosted-button flow.
Embed Connect reference
The full embed surface: page mode vs. button mode, the postMessage protocol, and branding.
Remote pairing reference
Full body and options for the link-generating endpoint.
Configure webhooks
Per-number subscriptions, event lists, and delivery logs.