Skip to main content

WhatsApp signup for AI builders

You are building a CRM or SaaS on Lovable, Replit, Bolt, v0 or Cursor, and your customers need to connect their own official WhatsApp number. This page gives you four prompts. Paste them into your builder in order and you get a working signup flow — with no Facebook SDK on your page, no Meta app of your own, and no FB.login in the browser.
Rather start from working code? Download the MIT-licensed demo of this exact flow (Node backend + React frontend, with ready-made button presets): embedded-signup-demo.zip. Drop it into your builder, or run it locally with npm run dev.

Why the hosted button, and not FB.login on your page

Your builder publishes on a domain it owns — something.lovable.app, something.replit.app, a fresh preview URL on every deploy. That domain changes per project, and often per push. Meta validates the domain that runs FB.login. So putting the Facebook button directly on your own page would mean registering every one of those domains in Pilot Status’s Meta app, one by one, forever. That does not scale, and it is not offered. The hosted button solves this by running the whole signup inside an iframe served from connect.pilotstatus.com.br — a domain Pilot Status already owns and already registered. The Facebook SDK and the login popup live inside that iframe, not on your page. Facebook checks the iframe’s origin, not yours. Your page can live on any domain, including a preview URL that did not exist five minutes ago, and the popup opens normally. Nothing to register. Nothing to wait for. No SDK to load. This is the correct path for builder-hosted apps, not a workaround. It is the same flow documented in Embed the Connect page — this guide is the AI-builder, prompt-driven version of it.
If you own a stable domain and want the Facebook button on your own markup, use Embedded Signup in your own app instead. It gives you full control of the button, at the cost of running the Facebook JavaScript SDK yourself.

Before you start

  1. A Pilot Status account with a free number slot on your plan.
  2. A tenant-scoped API key (ps_...) from the API tab of your profile (/profile). A number-scoped key will not work — generating a signup link creates a brand new number, so the key must not be bound to an existing one.
  3. A backend. Every builder listed above can run one: a Replit server, a Next.js route handler on v0, a Supabase Edge Function on Lovable or Bolt. You need it because the browser cannot call the Pilot Status API directly — the only thing your backend does here is generate the connection link.
The ps_ key is a server-side secret. It authorizes everything in your account. It must never appear in frontend code, and never in a variable prefixed VITE_, NEXT_PUBLIC_, REACT_APP_ or PUBLIC_ — those are compiled into the JavaScript bundle your users download. Every prompt below repeats this, because AI builders get it wrong by default.

How the pieces fit

Your page never loads the Facebook SDK, never serves an appId or config_id, never reads /api/health, and never calls FB.login. The ps_ key only ever exists on the bottom row.

The four prompts

Paste them in order. Each one is self-contained — the AI reading it cannot see this page, so everything it needs is written into the prompt itself.
Generating the link creates a placeholder number and consumes a plan slot immediately. Generate it when the customer is actually about to click, not on page load. A link the customer abandons keeps its slot until you delete the placeholder number from the /numbers dashboard — there is no automatic cleanup.

Prompt 2 — Frontend iframe with the correct sandbox

Prompt 3 — Handle connect:paired and save the number

Prompt 4 — Register the webhook that delivers inbound messages

Mistakes the AI usually makes

These five account for nearly every broken integration. If something does not work, check them in this order.
The builder reads it from VITE_PILOT_KEY or NEXT_PUBLIC_PILOT_KEY because that is the fastest way to make a fetch call compile. Those prefixes inline the value into the shipped bundle, so anyone who opens DevTools owns your account. The key belongs in a plain server-side variable such as PILOT_TENANT_KEY, read only from the backend route that generates the connection link. If you find it in the bundle, rotate the key in the dashboard before fixing the code.
Without allow-popups and allow-popups-to-escape-sandbox, the Facebook popup is blocked by the browser with no error, no console warning and no visible failure. The button simply does nothing when clicked, which sends people hunting for a bug in their token logic. The full attribute is sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox".
The generated code does fetch("https://pilotstatus.com.br/v1/...") from a component. CORS on the API is a strict allowlist of Pilot Status’s own domains, so a request from *.lovable.app or *.replit.app never receives an Access-Control-Allow-Origin header and is blocked. No API key changes this. In the hosted flow the browser never calls the Pilot Status API at all — it only embeds the iframe and calls your own backend, and your backend is the one that generates the connection link.
POST /v1/webhooks accepts a body with no events, returns success, and then delivers nothing. There is no warning. Send an explicit array — ["messages"] for a META number, or ["*"] for everything.
The AI knows the standard Facebook Embedded Signup recipe and will happily pull in connect.facebook.net/en_US/sdk.js, serve an appId/config_id, and call FB.login on your page. In this flow you do none of that: the SDK, the popup, and the Meta app all live inside the hosted iframe, on a domain that is already registered with Meta. Your *.lovable.app or *.replit.app URL is not — nor can it be, since it changes per project and per deploy. If a prompt starts loading the Facebook SDK, serving appId/configId, reading /api/health, or calling FB.login, you are on the wrong path — delete it and let the iframe do the work.

A note on button styling

The hosted button’s appearance comes from the branding.button object you send when you generate the connection link — see Prompt 1. That request is authenticated with your ps_ key, so the styling is always attributable to your account. There is deliberately no query parameter for button styling. Do not try to pass colours, labels or logos on the iframe URL; they will be ignored. The “secured by pilotstatus.com.br” marker below the button renders by default. branding.button.hideProvenance can suppress it, but only in button mode and only when that capability is enabled for your account — otherwise the value is ignored and the marker still renders. Contact support if you need it.

Next steps

The number is connected and webhooks are flowing. Now send something.

Send messages

Your first send with POST /v1/messages/send — templates, free-form text and media.

Embed the Connect page

The full reference for the hosted flow — token shape, mode=button, the postMessage protocol and branding.

Embedded Signup in your own app

The full-control alternative, for when you own a stable domain and want the Facebook SDK on your own markup.