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.
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. Descompacte, ponha a sua chave
ps_, npm run dev (ou docker compose up).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, é 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.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.Uma credencial só
Você precisa de exatamente uma coisa: uma chave de APIps_ 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
message — nada mais.
Os quatro passos
1
Seu backend gera o link de conexão
Chame Esse exemplo gera um link cuja página é só o caso mais pedido: botão verde WhatsApp (Guarde a O id do número placeholder não vem no corpo da resposta — ele só existe na claim
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:cURL
#25D366) com o logo do WhatsApp antes do rótulo, cantos arredondados e 56 px de altura.A resposta traz exatamente três campos: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_.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 depostMessagede volta pra sua página. Sem eles o botão renderiza e funciona igual — você só nunca fica sabendo do resultado.
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. Os três que mais derrubam gente:- O
iconsegue avariantquando você omite.facebook→ glifo do Facebook,whatsapp→ glifo do WhatsApp, qualquer outra variante → nenhum glifo. Mande"none"pra tirar de propósito. - O
widthsubstitui ofullWidth. Se você mandar largura, ofullWidthé 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 errorcom umdetails— e nenhum link, nenhum placeholder, nenhum slot consumido.height: 1000não cai no padrão; derruba a chamada inteira.
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.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.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.2
Embeda o iframe hospedado
Com o 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
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.Frontend — HTML + JS
branding.button que você mandou no passo 1.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.3
Escute o evento connect:paired
O iframe posta o resultado para a sua página por O payload de
O
postMessage. Registre um listener de message e valide a origem — event.origin === "https://connect.pilotstatus.com.br" — antes de confiar em qualquer coisa.Frontend — HTML + JS
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; o id é o numberId que você leu da claim do JWT no passo 1.Backend — devolve o slot ao abandonar
4
Cadastre o webhook
Um número conectado por este fluxo é um número META. Inscreva-o para receber as mensagens que chegarem. Use o O
numberId que você leu da claim do JWT (passo 1) ou recebeu no connect:paired (passo 3):cURL
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.Envelope nativo (recorte)
Backend de exemplo
O seu backend só precisa de um endpoint: ele gera o link e devolve oconnectUrl (e, opcionalmente, o numberId decodificado). Nenhuma rota de config, nenhuma rota de complete — o complete acontece dentro do iframe hospedado.
Backend — Node.js / Express
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:
cURL
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.
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 depostMessagemanual, a referência completa do embed — incluindoPilotStatus.connect.mount(...)no modo botão — está em Incorpore a Página de Conexão. - 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.
- 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, ou cadastre o número direto comPOST /v1/numbers/meta— veja Cadastrar número.
Próximos passos
- Referência completa do embed hospedado: Incorpore a Página de Conexão
- Prompts para construtores de IA: Embedded Signup para construtores de IA
- Referência do endpoint que gera o link: POST /v1/numbers/remote-pairing
- Escopos de chave e cabeçalhos: Autenticação da API
- Receber as mensagens do número recém-conectado: Receber mensagens
- Primeiro envio pelo número recém-conectado: Enviar mensagens