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

# Meta Embedded Signup no seu app com botão hospedado | Pilot Status

> Conecte o número WhatsApp oficial (Cloud API) do seu cliente embutindo a página de conexão hospedada da Pilot Status num iframe (mode=button) — o botão Continuar com o Facebook e o popup já vivem lá dentro. Sem SDK do Facebook, sem app Meta próprio, uma única chave ps_ de tenant.

# Embedded Signup no seu app com botão hospedado

Conecte o número WhatsApp oficial (Cloud API) do seu cliente sem tirá-lo do seu produto — **e sem carregar o SDK do Facebook**. Você embute a página de conexão hospedada da Pilot Status num `<iframe>` no modo botão (`mode=button`); o botão **Continuar com o Facebook** e o popup do Meta Embedded Signup já vivem dentro dessa página, servida por `connect.pilotstatus.com.br`.

Seu app faz só duas coisas: o **backend gera o link de conexão** com a sua chave de tenant, e o **frontend embute o iframe** e escuta o resultado.

<Note>
  Quer um ponto de partida pronto pra rodar? Baixe o demo (licença MIT) — backend Node e frontend React ligados exatamente a este fluxo, com modelos de botão prontos: [**embedded-signup-demo.zip**](https://pilotstatus.com.br/downloads/embedded-signup-demo.zip). Descompacte, ponha a sua chave `ps_`, `npm run dev` (ou `docker compose up`).
</Note>

<Info>
  **Esta página é o passo a passo**: siga os quatro passos na ordem e você tem uma integração funcionando. A página irmã, [**Incorpore a Página de Conexão**](/pt-BR/integrations/embed-connect), é a *referência* — todas as opções, os dois providers (Cloud API e QR), o protocolo postMessage completo e a tabela cheia do `branding.button`. Esta página linka pra lá em vez de repetir, então você sempre lê uma única cópia.
</Info>

<Note>
  **Este é o caminho recomendado.** Ele evita todo o trabalho de rodar o Embedded Signup na sua própria página. Seu app não carrega SDK nenhum do Facebook e não roda `FB.login` — não há nada de app Meta para você provisionar. Nenhum domínio seu precisa ser registrado num app Meta: o botão roda em `connect.pilotstatus.com.br`, um domínio já registrado no app Meta da Pilot Status.
</Note>

## Uma credencial só

Você precisa de exatamente uma coisa: uma chave de API `ps_` com **escopo de tenant**. Pegue em `/profile`, aba **API**.

Você **não** precisa de app Meta próprio, nem de App Review, nem de nenhuma configuração do lado da Meta, nem de chave com escopo de número. O Embedded Signup roda sobre o app da Pilot Status.

A chave `ps_` fica **no seu servidor** — o browser nunca chama a API da Pilot Status, ele só embeda o iframe.

## Arquitetura

```text theme={null}
Browser (sua página)
   │ 1. POST /api/onboarding/start   → { connectUrl }   (seu backend gerou o link)
   │ 2. <iframe src="connect.pilotstatus.com.br/connect/<token>?embed=1&mode=button">
   │       ↑ o SDK do Facebook e o popup vivem AQUI DENTRO, na página da Pilot
   │ 3. window "message" → connect:paired → { numberId, externalRef, ... }
   ▼
Seu backend ── x-api-key: ps_ ──►  https://pilotstatus.com.br/v1/numbers/remote-pairing
```

Só o seu backend toca na API da Pilot Status. O browser embeda o iframe e escuta um evento de `message` — nada mais.

## Os quatro passos

<Steps>
  <Step title="Seu backend gera o link de conexão">
    Chame `POST /v1/numbers/remote-pairing` com `provider: "META"` e `metaFlow: "embedded"`, usando a chave de tenant. Para estilizar o botão, mande um objeto `branding.button`:

    ```bash cURL theme={null}
    curl -X POST "https://pilotstatus.com.br/v1/numbers/remote-pairing" \
      -H "Content-Type: application/json" \
      -H "x-api-key: ps_sua_chave_tenant" \
      -d '{
        "provider": "META",
        "metaFlow": "embedded",
        "name": "Cliente João",
        "externalRef": "crm-org-42",
        "branding": {
          "button": {
            "variant": "whatsapp",
            "label": "Conectar meu WhatsApp",
            "icon": "whatsapp",
            "radius": "pill",
            "size": "lg",
            "height": 56,
            "fullWidth": true
          }
        }
      }'
    ```

    Esse exemplo gera um link cuja página é só o caso mais pedido: botão verde WhatsApp (`#25D366`) com o logo do WhatsApp antes do rótulo, cantos arredondados e 56 px de altura.

    A resposta traz exatamente três campos:

    ```json theme={null}
    {
      "provider": "META",
      "remotePairingUrl": "https://connect.pilotstatus.com.br/connect/eyJhbGciOiJIUzI1NiIs...",
      "expiresAt": "2026-07-20T18:30:00.000Z"
    }
    ```

    Guarde a `remotePairingUrl` — é o link de conexão (`connectUrl`) que o frontend vai embutir. Devolva **só** esse link (ou o token) ao seu frontend, nunca a chave `ps_`.

    <Tip>
      **Confira em cinco segundos: cole a `remotePairingUrl` no navegador.** Porque você mandou `branding.button`, esse link renderiza **só o seu botão estilizado** — o botão verde WhatsApp de 56 px do exemplo acima. É exatamente o que o seu cliente vê dentro do iframe no passo 2. (Em nível superior aparece também o marcador pequeno "secured by pilotstatus.com.br"; dentro de um iframe de verdade ele se esconde sozinho.)

      Viu um card inteiro, com cabeçalho e texto explicativo? Então o link **não** carrega `branding.button` — confira se você aninhou o objeto dentro de `branding`, e não no topo do corpo.
    </Tip>

    Vale ser preciso aqui, porque as duas metades moram em lugares diferentes:

    * **Esta chamada decide o *estilo* e a *forma*.** O `branding.button` é assinado dentro do token, e mandá-lo é também o que faz o link renderizar o botão puro em vez da página cheia. Um objeto, os dois efeitos.
    * **Os parâmetros `?embed=1&parentOrigin=…` do passo 2 decidem o *encanamento*.** São eles que ligam o canal de `postMessage` de volta pra sua página. Sem eles o botão renderiza e funciona igual — você só nunca fica sabendo do resultado.

    **O que o `branding.button` aceita.** Os dez campos são todos opcionais, e a tabela completa — valores, padrões, faixas, mais o `hideProvenance` — está na referência: [Estilo do botão](/pt-BR/integrations/embed-connect#estilo-do-botao-somente-via-token). Os três que mais derrubam gente:

    * **O `icon` segue a `variant` quando você omite.** `facebook` → glifo do Facebook, `whatsapp` → glifo do WhatsApp, **qualquer outra variante → nenhum glifo**. Mande `"none"` pra tirar de propósito.
    * **O `width` substitui o `fullWidth`.** Se você mandar largura, o `fullWidth` é ignorado, seja o que for que você pôs nele.
    * **Valor inválido é recusado, não corrigido.** Fora da faixa ou fora do enum devolve `400 Validation error` com um `details` — e **nenhum link, nenhum placeholder, nenhum slot consumido**. `height: 1000` não cai no padrão; derruba a chamada inteira.

    <Note>
      O `branding.button` **só** pode ser definido aqui, ao gerar o link — nunca por parâmetro de query na URL do iframe. É por segurança: quem vê a URL do iframe não consegue repintar o seu botão, e toda página estilizada fica atribuível a um tenant.
    </Note>

    O **id do número placeholder não vem no corpo da resposta** — ele só existe na claim `numberId` do JWT. Decodifique o payload em base64url (é leitura de metadado; não é preciso verificar a assinatura) para lê-lo agora e já cadastrar o webhook (passo 4) antes de o cliente concluir. Você também recebe esse `numberId` no evento `connect:paired`, no passo 3. Esses dois são os caminhos suportados — **guarde o id assim que decodificar**, junto do seu cadastro de cliente, porque é ele que você vai usar depois para apagar um placeholder abandonado.

    <Warning>
      **Gerar o link já consome um slot do plano.** A chamada cria um `WhatsAppNumber` placeholder na hora. Um link abandonado **vaza o slot** — não existe rotina de limpeza automática. Gere o link só depois que o cliente clicar em conectar, e devolva o slot quando a sessão terminar sem um `connect:paired` (passo 3) — o `DELETE` está em [Limpe placeholders órfãos](#limpe-placeholders-órfãos).

      É aqui, e não no complete, que o slot é cobrado. Sem slot disponível a chamada responde **`402`** e nenhum link e nenhum placeholder são criados. Leia o `code`, porque só um dos dois é sobre dinheiro: `PLAN_NUMBER_LIMIT_REACHED` significa que a cota do próprio plano está cheia e nenhum extra foi comprado — libere um slot em `/numbers` ou suba de plano, já que crédito não resolve; `INSUFFICIENT_FUNDS` significa que o número seria um extra pago que não dá para custear — adicione créditos ou salve um cartão. O corpo traz `plan`, `maxNumbers` e `currentNumberCount` para você saber dizer ao seu cliente qual é o caso. Veja [erros de capacidade](/pt-BR/api/numbers/create#erros-de-capacidade-e-como-resolver).
    </Warning>

    <Note>
      O token é o último segmento da `remotePairingUrl` (`/connect/<token>`). Ele é um **segredo bearer de 30 minutos** (HS256, TTL de 30 min), não é de uso único. No modelo hospedado ele viaja na URL do iframe — isso é inerente a embutir a página da Pilot: quem tiver o link pode concluir o fluxo enquanto ele valer. Gere o link **na hora** em que o cliente vai clicar, não antes.
    </Note>
  </Step>

  <Step title="Embeda o iframe hospedado">
    Com o `connectUrl` em mãos, o **frontend** monta a URL do iframe. Você anexa três parâmetros: `embed=1`, `mode=button` e `parentOrigin` — este último é `location.origin`, que só o browser sabe.

    | Parâmetro      | Por que ele está aí                                                                                                                                                                                                                                                                                       |
    | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `embed=1`      | **Obrigatório.** Liga o canal de `postMessage` pra sua página. Sem ele o botão funciona, mas você nunca recebe o `connect:paired`.                                                                                                                                                                        |
    | `parentOrigin` | **Obrigatório junto com `embed=1`.** É a única origem pra qual o iframe posta. Tem que ser o `location.origin` exato da sua página.                                                                                                                                                                       |
    | `mode=button`  | **Opcional aqui**, porque o link já carrega o `branding.button` do passo 1. Mantenha de todo jeito — é uma palavra que deixa a forma explícita e continua funcionando se depois você gerar um link *sem* estilo customizado. O oposto dele, `mode=page`, força o card completo mesmo num link estilizado. |

    ```html Frontend — HTML + JS theme={null}
    <div id="connect-slot"></div>

    <script>
      // connectUrl = a remotePairingUrl que o SEU backend devolveu (nunca a chave ps_).
      async function embedConnect() {
        const { connectUrl } = await fetch("/api/onboarding/start", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ name: "Cliente João", externalRef: "crm-org-42" }),
        }).then((r) => r.json());

        const src =
          connectUrl +
          "?embed=1&mode=button&parentOrigin=" +
          encodeURIComponent(location.origin);

        const iframe = document.createElement("iframe");
        iframe.src = src;
        iframe.sandbox =
          "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms";
        iframe.style.cssText = "width:100%;border:0;display:block";
        iframe.style.height = "60px"; // ajustada pelo evento `resize`
        document.getElementById("connect-slot").appendChild(iframe);
      }
      embedConnect();
    </script>
    ```

    O SDK do Facebook e o popup do Meta Embedded Signup vivem **dentro** desse iframe hospedado — **seu app não carrega SDK nenhum**. O botão renderiza a partir do `branding.button` que você mandou no passo 1.

    <Warning>
      O sandbox **precisa** de `allow-popups` **e** `allow-popups-to-escape-sandbox`. A janela de login do Facebook abre de dentro do frame; sem esses dois, o navegador bloqueia o popup e **não acontece nada** — sem erro, sem callback, sem nada no console. Copie o sandbox exatamente como está acima.
    </Warning>

    <Note>
      No modo botão a página renderiza **só o botão**, e o fundo dela fica **transparente** — isso é propriedade do próprio `mode=button`, não de estar num iframe —, então o fundo do seu app aparece atrás. Já o pequeno marcador **"secured by pilotstatus.com.br"** é o que depende do iframe: dentro de um iframe de verdade ele fica **escondido por padrão** (você não precisa pedir). Aberto em nível superior (o mesmo link sem iframe em volta), o marcador **aparece**, de propósito: um botão de login estilizado sozinho numa URL `*.pilotstatus.com.br`, sem procedência visível, é exatamente o que o Safe Browsing marca como enganoso.
    </Note>
  </Step>

  <Step title="Escute o evento connect:paired">
    O iframe posta o resultado para a sua página por `postMessage`. Registre um listener de `message` e **valide a origem** — `event.origin === "https://connect.pilotstatus.com.br"` — antes de confiar em qualquer coisa.

    ```html Frontend — HTML + JS theme={null}
    <script>
      const CONNECT_ORIGIN = "https://connect.pilotstatus.com.br";
      // numberId veio junto com o connectUrl (seu backend decodificou a claim do JWT).
      let conectado = false;

      window.addEventListener("message", (event) => {
        // Dois cheques obrigatórios: a origem tem que ser o host do connect, e
        // toda mensagem carrega o marcador `source: "pilot-status-embed"`. Sem o
        // segundo, qualquer script na sua página poderia forjar um connect:paired.
        if (event.origin !== CONNECT_ORIGIN) return;
        const msg = event.data;
        if (!msg || msg.source !== "pilot-status-embed") return;

        // Envelope: { source, v, type, payload }. Só o `type` fica no topo; o
        // resto vem em `payload`.
        const p = msg.payload || {};
        switch (msg.type) {
          case "connect:paired": {
            // p = { numberId, phone, displayName, provider, externalRef, redirectUrl }
            // Cruze pelo externalRef que VOCÊ passou ao gerar o link.
            conectado = true;
            salvarNumeroConectado(p.numberId, p.externalRef);
            break;
          }
          case "connect:expired":
            // Token de 30 min expirou — gere um novo link e re-embeta.
            break;
          case "connect:error":
            // Chega também quando a pessoa CANCELA ou fecha o popup do Facebook.
            // O botão continua na tela para ela tentar de novo — aqui só anote.
            // Cancelou o popup? Ela ainda pode clicar de novo — só limpe quando
            // tiver certeza de que foi embora (ex.: fechou o modal, saiu da página).
            console.error(p.message);
            break;
          case "resize":
            // Ajuste a altura do iframe conforme a página hospedada cresce.
            document.querySelector("#connect-slot iframe").style.height = p.height + "px";
            break;
        }
      });

      // Chame quando a pessoa realmente foi embora: você fechou o modal,
      // ela saiu da página, ou você desistiu da sessão.
      async function encerrarOnboarding(numberId) {
        if (conectado) return; // conectou: o número é real, não apague.
        // Só o SEU backend tem a chave ps_ — é ele que chama DELETE /v1/numbers/{id}.
        await fetch("/api/onboarding/abandon", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ numberId }),
        });
      }
    </script>
    ```

    O payload de `connect:paired` traz `numberId`, `phone`, `displayName`, `provider`, `externalRef` e `redirectUrl` — todos sempre presentes (um fluxo que não conhece algum envia `null`, nunca omite). Cruze pelo `externalRef` para casar o número com o cadastro de cliente do seu lado. O `numberId` é o id usado em todo o resto da API (`GET /v1/numbers/{id}`, webhooks, envio). Remova o listener no cleanup da sua página/componente.

    ### `connect:error` também é o seu sinal de "o cliente desistiu"

    O `connect:error` chega numa falha terminal **e** quando a pessoa **cancela ou fecha o popup do Facebook** — o Facebook não distingue as duas coisas, então ambas chegam pelo mesmo evento, com o mesmo payload `{ message }`. Nos dois casos o botão **continua na tela**, e a pessoa pode simplesmente clicar de novo.

    Isso importa por causa do custo: o número placeholder criado no passo 1 já está segurando um slot do plano. O link expira em 30 minutos, mas o placeholder **não** some junto — ele fica lá segurando o slot até alguém apagar. Em vez de deixar isso acontecer, rode uma limpeza **delete-on-abandon** sempre que a sessão terminar **sem** um `connect:paired` — você fechou o modal, a pessoa saiu da página, ou você desistiu da sessão — e **não** no primeiro `connect:error`, que ela ainda pode tentar de novo. O browser avisa o **seu** backend (ele não tem chave `ps_`), e o seu backend apaga o placeholder com `DELETE /v1/numbers/{id}`, devolvendo o slot na hora. A chamada está em [Limpe placeholders órfãos](#limpe-placeholders-órfãos); o id é o `numberId` que você leu da claim do JWT no passo 1.

    ```javascript Backend — devolve o slot ao abandonar theme={null}
    // POST /api/onboarding/abandon  { numberId }
    // BASE e TENANT_KEY são as mesmas constantes do "Backend de exemplo", abaixo.
    app.post("/api/onboarding/abandon", async (req, res) => {
      const { numberId } = req.body || {};
      const r = await fetch(`${BASE}/v1/numbers/${numberId}`, {
        method: "DELETE",
        headers: { "x-api-key": TENANT_KEY },
      });
      // 200 { ok: true } — slot devolvido. 404 = já não existe: nada a fazer.
      res.status(r.ok ? 200 : r.status).json(await r.json().catch(() => ({})));
    });
    ```

    <Warning>
      **Não delete no primeiro `connect:error`.** O botão continua ali para uma nova tentativa — e apagar o placeholder **não invalida o link**: o token é verificado só pela assinatura e pela validade, sem consultar o banco. Se a pessoa clicar de novo depois do seu `DELETE`, o fluxo conclui assim mesmo e a Pilot Status cria um número **novo**, consumindo um slot novo (o limite do plano é reavaliado nessa hora). Ou seja, deletar cedo demais não te protege: te dá um segundo número inesperado. Dispare a limpeza só quando a pessoa realmente foi embora: fechou o seu modal, saiu da página, ou você desistiu da sessão.
    </Warning>
  </Step>

  <Step title="Cadastre o webhook">
    Um número conectado por este fluxo é um número **META**. Inscreva-o para receber as mensagens que chegarem. Use o `numberId` que você leu da claim do JWT (passo 1) ou recebeu no `connect:paired` (passo 3):

    ```bash cURL theme={null}
    curl -X POST "https://pilotstatus.com.br/v1/webhooks" \
      -H "Content-Type: application/json" \
      -H "x-api-key: ps_sua_chave_tenant" \
      -d '{
        "url": "https://seu-crm.com/hooks/whatsapp",
        "whatsappNumberId": "<numberId>",
        "events": ["*"]
      }'
    ```

    O `events` é **obrigatório** e precisa ser explícito: uma lista vazia não entrega nada. Use `["*"]` para receber tudo (incluindo os eventos de saúde do número, que só chegam pelo curinga). Como o `numberId` já é o id definitivo desde o passo 1, você pode cadastrar o webhook **antes** de o cliente concluir — assim não perde as primeiras mensagens. As entregas usam BullMQ com **5 tentativas** e backoff exponencial a partir de 5 segundos.

    <Warning>
      **Número META entrega o envelope nativo da Meta**, com `entry[].changes[].field: "messages"` — e **nunca** `message.received`. O evento `message.received` é vocabulário de números não oficiais (Pilot Status web). Inscrever-se em `events: ["*"]` **não** converte o envelope: você continua recebendo o formato nativo. Escreva o parser do seu receptor para o formato nativo (ou ramifique pelo provedor do número).

      Todo nome de campo que você pode assinar num número Meta está listado em [Eventos da Meta Cloud API](/pt-BR/api/webhooks/events#eventos-da-meta-cloud-api-envelope-nativo) — `messages` é o que você quer para conversas.
    </Warning>

    ```json Envelope nativo (recorte) theme={null}
    {
      "object": "whatsapp_business_account",
      "entry": [
        {
          "id": "987654321098765",
          "changes": [
            {
              "field": "messages",
              "value": {
                "messaging_product": "whatsapp",
                "metadata": { "display_phone_number": "5511999999999", "phone_number_id": "123456789012345" },
                "messages": [{ "from": "5585984387245", "id": "wamid...", "type": "text", "text": { "body": "Oi" } }]
              }
            }
          ]
        }
      ]
    }
    ```

    <Warning>
      **Hoje nenhum cabeçalho de assinatura é emitido** para webhooks criados pela API pública: o campo `secret` nunca é gravado, então `x-pilot-status-signature` não chega. Não escreva verificação de assinatura contando com ele. Autentique as entregas por outro meio — URL secreta e imprevisível, mTLS ou allowlist de IP.
    </Warning>
  </Step>
</Steps>

## Backend de exemplo

O seu backend só precisa de um endpoint: ele gera o link e devolve o `connectUrl` (e, opcionalmente, o `numberId` decodificado). Nenhuma rota de `config`, nenhuma rota de `complete` — o complete acontece dentro do iframe hospedado.

```javascript Backend — Node.js / Express theme={null}
import express from "express";

const app = express();
app.use(express.json());

const BASE = (process.env.PILOT_API_BASE || "https://pilotstatus.com.br").replace(/\/$/, "");
const TENANT_KEY = process.env.PILOT_TENANT_KEY; // ps_... com escopo de tenant — SÓ no servidor

// O id do número placeholder só existe na claim `numberId` do JWT — não vem no corpo.
// Só leitura de metadado: não verificamos a assinatura aqui.
function readNumberId(token) {
  try {
    const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8"));
    return payload.numberId ?? null;
  } catch {
    return null;
  }
}

app.post("/api/onboarding/start", async (req, res) => {
  const button = req.body?.button; // objeto branding.button (opcional)

  const r = await fetch(`${BASE}/v1/numbers/remote-pairing`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "x-api-key": TENANT_KEY },
    body: JSON.stringify({
      provider: "META",
      metaFlow: "embedded",
      name: req.body?.name,
      externalRef: req.body?.externalRef,
      branding: button ? { button } : undefined,
    }),
  });
  const data = await r.json();
  if (!r.ok) {
    // Ex.: nenhum slot livre no plano — o placeholder não pôde ser criado.
    return res.status(r.status).json({ error: data?.error || data?.message || `Erro ${r.status}` });
  }

  // ATENÇÃO: esta chamada já consumiu um slot do plano (criou o número placeholder).
  const token = new URL(data.remotePairingUrl).pathname.split("/").pop();

  // O browser recebe SÓ o link de conexão. A chave ps_ nunca sai daqui.
  res.status(201).json({
    connectUrl: data.remotePairingUrl,
    numberId: readNumberId(token),
    expiresAt: data.expiresAt,
  });
});

app.listen(process.env.PORT || 8788);
```

<Warning>
  A chave `ps_` é um segredo de servidor. **Nunca** a coloque numa variável exposta ao browser (`VITE_...`, `NEXT_PUBLIC_...`) nem a devolva num JSON que o frontend possa ler. O frontend recebe apenas o `connectUrl`.
</Warning>

## Limpe placeholders órfãos

Cada link gerado cria um número placeholder que ocupa um slot. Se o cliente abandonar o fluxo, o placeholder fica órfão e o slot fica preso. Remova-o pelo painel `/numbers` ou pela API:

```bash cURL theme={null}
curl -X DELETE "https://pilotstatus.com.br/v1/numbers/<numberId>" \
  -H "x-api-key: ps_sua_chave_tenant"
```

Responde `200 { "ok": true }` (ou `404 "Number not found"` se o id não existir). Aceita o id do número **ou** o id da instância. Use o `numberId` que você guardou no passo 1 (a claim `numberId` do JWT, ou o mesmo id que chega no `connect:paired`) para liberar o slot de um placeholder abandonado — de preferência automatizando quando a sessão do passo 3 acabar sem `connect:paired`. Esperar o link expirar não resolve: o placeholder continua ocupando o slot depois dos 30 minutos.

<Warning>
  Apagar o placeholder **não revoga o link**. O token é validado só pela assinatura e pela validade — não há consulta ao banco —, então, enquanto ele valer, uma nova tentativa do cliente ainda conclui e cria um número **novo**, consumindo outro slot. Delete só quando tiver certeza de que ninguém vai tentar de novo.
</Warning>

## Outros caminhos

O botão hospedado é a recomendação — mantém seu app sem SDK do Facebook e sem app Meta próprio. Ainda assim, existem alternativas conforme o seu caso:

* **SDK de incorporação, callbacks JS.** Se você prefere um SDK com callbacks (`onPaired`, `onError`, `onExpired`) em vez de um listener de `postMessage` manual, a referência completa do embed — incluindo `PilotStatus.connect.mount(...)` no modo botão — está em [Incorpore a Página de Conexão](/pt-BR/integrations/embed-connect).
* **Construtores de IA.** Se você usa v0, Lovable, Cursor ou Bolt, há prompts prontos que já geram este mesmo fluxo hospedado em [Embedded Signup para construtores de IA](/pt-BR/guides/embedded-signup-ai-builders).
* **Cliente que já tem WABA própria.** Se o cliente já vai colar as credenciais da Cloud API dele na mão, o popup do Facebook não entra na história — use o fluxo `credentials` (`metaFlow: "credentials"`), documentado na [referência do embed](/pt-BR/integrations/embed-connect), ou cadastre o número direto com `POST /v1/numbers/meta` — veja [Cadastrar número](/pt-BR/api/numbers/create).

## Próximos passos

* Referência completa do embed hospedado: [Incorpore a Página de Conexão](/pt-BR/integrations/embed-connect)
* Prompts para construtores de IA: [Embedded Signup para construtores de IA](/pt-BR/guides/embedded-signup-ai-builders)
* Referência do endpoint que gera o link: [POST /v1/numbers/remote-pairing](/pt-BR/api/numbers/remote-pairing)
* Escopos de chave e cabeçalhos: [Autenticação da API](/pt-BR/api/authentication)
* Receber as mensagens do número recém-conectado: [Receber mensagens](/pt-BR/guides/receive-messages)
* Primeiro envio pelo número recém-conectado: [Enviar mensagens](/pt-BR/guides/send-messages)
