Skip to main content
Incorpore o fluxo de pareamento de número (Connect) do WhatsApp — QR code (não oficial) ou Meta Embedded Signup (oficial) — no seu próprio SaaS, white-label. Os pré-requisitos compartilhados (SDK, chave de tenant) estão em Incorpore a Caixa de Entrada do Chat.
Página de conexão hospedada com logotipo, cores e título personalizáveis e um botão Conectar Dispositivo

A página de conexão hospedada que seu cliente vê — logotipo, cores e título personalizáveis, com um único botão Conectar Dispositivo.

Esta página é a referência — todas as opções, os dois providers, o protocolo postMessage completo e a tabela cheia do branding.button. Se você está ligando o onboarding da API oficial pela primeira vez, comece pelo passo a passo: Embedded Signup no seu app percorre as quatro chamadas de ponta a ponta, com código de backend e frontend pra copiar. Volte aqui para o detalhe campo por campo.
Diferença principal em relação ao Chat: o Connect não usa o token de POST /v1/embed/sessions. Ele usa o token de pareamento remoto gerado por POST /v1/numbers/remote-pairing, e esse token fica no caminho da URL do iframe (/connect/<token>), não em memória via postMessage.

Formato e validade do token

O token devolvido depende do provider — não são o mesmo objeto e não compartilham a mesma validade: Gere o link Meta no momento em que o usuário vai clicar, não horas antes — 30 minutos é curto de propósito. Quando expira, o iframe emite connect:expired; gere um novo.

Fluxo

1

O backend cria o link de pareamento

201 nos dois casos. remotePairingUrl é https://connect.pilotstatus.com.br/connect/<token>; o token é o último segmento do caminho. Encaminhe apenas o token (ou a URL) para o seu frontend — nunca a chave ps_.Para provider: "META", name/number não são obrigatórios: o usuário final escolhe o número dentro do diálogo do Facebook. Para PILOT_STATUS, ambos são obrigatórios.warnings no pareamento por QR. O externalRef e o redirectUrl viajam dentro da sessão assinada do Meta, e o link de QR não tem sessão nenhuma — então no PILOT_STATUS eles são aceitos, mas não têm como ser honrados. Cada um que você enviar volta relatado no array warnings (string[]) do 201, e o pareamento continua dando certo; não é um 400. O campo não aparece quando não há nada a relatar. Leia esse array nos seus testes de integração — é assim que você descobre que um valor enviado foi ignorado em vez de aplicado.
O linkToApiKey foi aposentado — e o linkedApiKeyId vem sempre null. A opção saiu das três rotas de provisionamento (POST /v1/numbers, POST /v1/numbers/meta, POST /v1/numbers/remote-pairing). Enviá-la continua sendo aceito, então quem já manda esse campo hoje não quebra, mas ela não faz mais nada: nenhuma chave de API é tocada e o linkedApiKeyId do 201 nunca traz um id. Ela costumava reapontar uma chave com escopo de número para o número recém-criado, tirando o escopo do número ao qual ela estava de fato vinculada.
O campo de branding por link é branding (não brandingOverride). externalRef (máx. 200 caracteres) é o id do cliente no seu sistema — ele é assinado dentro do token Meta e devolvido para você no connect:paired.
2

O frontend incorpora

O SDK monta a URL do iframe a partir do token — você passa apenas o token:
ConnectOptions: token (obrigatório), baseUrl? (padrão https://connect.pilotstatus.com.br), mode? ("page" | "button" — omita para automático, veja Modo botão), onPaired(d), onError(e), onExpired().

Modo botão

O modo botão renderiza apenas o botão do Meta Embedded Signup — sem cabeçalho, sem card, sem linha do tempo, fundo transparente e altura controlada pela mensagem resize já existente. Todo o resto (token, protocolo postMessage, callbacks) é idêntico ao modo página. Use quando você já tem a sua própria tela de onboarding e o card hospedado seria uma segunda UI competindo dentro dela. Use o modo página quando quiser o fluxo explicativo completo, ou para pareamento por QR. Um link gerado com branding.button renderiza o botão por padrão. Mandar esse objeto é o pedido do botão — não existe uma segunda flag pra lembrar, nem como estilizar um botão e ser surpreendido por um card. Três entradas decidem, nesta ordem de precedência:
Isso mudou em 17/08/2026. Um link gerado com branding.button renderizava a página cheia a menos que o host também anexasse ?mode=button. Se você gera com branding.button e quer a página cheia de propósito — aí o estilo do token se aplica ao botão dentro do card —, anexe ?mode=page (ou passe mode: "page"). Nada mais muda: link sem estilo continua renderizando a página, e ?mode=button explícito continua renderizando o botão.
O branding.button só chega a um link pelo token assinado, então o modo automático só pode disparar num link cujo próprio backend pediu: não existe configuração no nível do tenant que vire os seus outros links, nem parâmetro de query que vire o link de outra pessoa. No modo botão a página também limpa o próprio fundo (html e body). Isso é característica do mode=button em si, não de estar dentro de um iframe — vale independentemente de como a URL for aberta. No embed normal, o efeito é que a sua página aparece através do iframe e o botão pousa direto no seu layout, em vez de ficar sobre um retângulo sólido perdido ali. Nada a configurar do seu lado.
O modo botão exige um token Meta gerado com metaFlow: "embedded". Um token metaFlow: "credentials" ignora o modo botão — pedido explicitamente ou alcançado pelo automático — e cai na página completa — o formulário de credenciais da Cloud API não cabe em um botão. Tokens de QR (PILOT_STATUS) também renderizam a página completa. Os estados de carregamento, erro de configuração, token expirado e sucesso continuam aparecendo no modo botão, em versão mínima e inline — nunca em tela cheia.
O iframe precisa do sandbox com allow-popups e allow-popups-to-escape-sandbox. A janela de login do Facebook abre de dentro do frame; sem allow-popups o navegador bloqueia e não acontece nada — sem erro, sem callback, sem mensagem no console. O SDK já define o sandbox correto (allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox); se você montar o iframe na mão, copie exatamente.
O Facebook valida a origem do iframe, não a da sua página de nível superior. É exatamente esse o ponto desta integração: o botão roda em connect.pilotstatus.com.br, um domínio já registrado no app Meta da Pilot Status, então você nunca precisa registrar o seu domínio (nem o do seu cliente).

Estilo do botão (somente via token)

A aparência do botão é definida na hora de gerar o link, dentro de branding.button, e viaja no token assinado. Mandar esse objeto também põe o link em modo botão por padrão, então esse único objeto é o pedido inteiro: estilize um botão e receba um botão.
O exemplo acima gera um link cuja página é só um botão verde WhatsApp com o logotipo do WhatsApp, 56 px de altura e 320 px de largura. Abra a remotePairingUrl devolvida no navegador para ver exatamente o que o seu cliente vai ver — a única diferença é o marcador “secured by”, que aparece em nível superior e se esconde dentro do seu iframe. Todos os campos são opcionais. O rótulo interno é a string em português Continuar com o Facebook — defina label explicitamente para qualquer outro idioma. Mantenha o objeto pequeno: ele viaja dentro do JWT que está na URL de conexão.
  • variant: "whatsapp" pinta o botão de verde WhatsApp (#25D366).
  • icon é o glifo de marca desenhado antes do rótulo — um SVG inline, sem nenhuma requisição de rede, que herda a cor do texto. Se você omitir o campo, ele segue a variante: facebook → glifo do Facebook, whatsapp → glifo do WhatsApp, qualquer outra variante → nenhum glifo. Envie icon: "none" para removê-lo explicitamente.
  • height é aplicado como altura mínima — o rótulo nunca corta — e substitui o espaçamento vertical que o size daria ao botão.
  • width substitui fullWidth: se você mandar width, o fullWidth é ignorado. Sem width, o botão ocupa toda a largura do contêiner por padrão; use fullWidth: false para encolhê-lo até o conteúdo.
  • Valide os valores antes de chamar. Valor fora da faixa ou fora do enum é recusado: o POST /v1/numbers/remote-pairing responde 400 Validation error com details, e nada é criado — sem link, sem placeholder, sem slot consumido. Um height de 1000 não cai no padrão, ele derruba a requisição inteira. (O sanitizador que roda no navegador realmente ignora campo a campo o que estiver fora da faixa e volta ao padrão, mas isso é defesa em profundidade para um valor antigo já assinado dentro de um token — pela API você nunca chega lá.)
Nada disso pode ser definido por parâmetro de query, e não há plano para permitir. O estilo chega somente por um token gerado com a sua chave ps_, de modo que toda página estilizada é atribuível a um tenant. Uma página cuja aparência qualquer visitante anônimo pudesse reescrever pela query string é exatamente a superfície de phishing que já fez um domínio da Pilot Status ser sinalizado — a sobreposição por query segue limitada aos parâmetros cosméticos já existentes, listados abaixo.

O marcador “secured by pilotstatus.com.br”

Se o pequeno marcador de procedência aparece embaixo do botão depende de onde a página está rodando — no caso normal, embedado, você não precisa configurar nada. São exatamente três situações:
  • No modo botão dentro de um iframe ele some sozinho. A página verifica se está mesmo dentro de um frame (window.parent !== window); se estiver, o marcador não é renderizado. Nenhum campo no token, nenhuma chave, nada a solicitar — é só embedar o botão e ele já não está lá.
  • Em nível superior o marcador fica, de propósito. Um botão de login sozinho, estilizado com a marca do tenant, numa URL *.pilotstatus.com.br e sem nada que identifique de quem é a página, é exatamente a superfície que o Safe Browsing marca como enganosa. É o marcador que mantém essa URL atribuível, então ele permanece sempre que o botão não estiver embedado.
  • A página completa mantém o marcador sempre, inclusive com hidePilotStatusBranding ligado.
branding.button.hideProvenance: true continua existindo como opt-out explícito para o caso do botão em nível superior, e continua atrelado a uma chave do lado do servidor controlada pela Pilot Status. Essa chave vem desligada por padrão, então definir hideProvenance sozinho não muda nada; fale com o suporte se você realmente precisar. Ele não pode ser definido por parâmetro de query, só no token gerado — e nunca afeta a página de conexão completa.

Protocolo postMessage (iframe → parent)

O Connect nunca recebe uma mensagem init — o token já está na URL. O SDK apenas escuta (validando origin === connect.pilotstatus.com.br e source === iframe.contentWindow):

Slots do plano e o padrão delete-on-abandon

Gerar o link já custa um slot do plano. O POST /v1/numbers/remote-pairing cria um número placeholder na hora, e um link abandonado continua segurando esse slot — não existe limpeza automática. Gere o link só quando o cliente estiver prestes a clicar.Quando não há vaga para mais um número, a chamada responde 402 — o mesmo corpo que o POST /v1/numbers devolve — e nenhum link e nenhum placeholder são criados. Dois códigos dividem esse status e não dividem a solução: PLAN_NUMBER_LIMIT_REACHED (a cota do próprio plano está cheia e nenhum extra foi comprado — libere um slot ou suba de plano; dinheiro não muda nada) e INSUFFICIENT_FUNDS (o número é um extra pago, sem crédito e sem cartão salvo). Ramifique pelo code e leia plan / maxNumbers / currentNumberCount no mesmo corpo. Esse caminho respondia 500 nesse caso, e reportava cota de plano cheia como INSUFFICIENT_FUNDS. Veja erros de capacidade.
Devolva o slot você mesmo, em vez de esperar o link caducar. O connect:error avisa que uma tentativa falhou — inclusive quando alguém fecha a janela do Facebook — mas ele não é o sinal para apagar: o botão continua na tela e a pessoa pode clicar de novo. Anote a tentativa e rode a limpeza quando a sessão realmente acabar sem um connect:paired (você fechou o modal, ela saiu da página, você desistiu):
Quando você desistir de vez, o seu backend apaga o placeholder criado para aquele cliente com DELETE /v1/numbers/{id} e a chave ps_ — o slot volta na hora, em vez de ficar preso até você perceber. A chamada de limpeza está escrita por extenso em Embedded Signup. O {id} desse placeholder chega até você por dois caminhos, e só por esses dois: ele é a claim numberId dentro do próprio token Meta — decodifique o payload base64url do JWT (o segmento do meio) no seu backend, é leitura de metadado, não precisa validar assinatura — e ele também vem no numberId do evento connect:paired. Guarde o id que você decodificou no momento em que gerou o link, junto com o seu externalRef, e a limpeza fica trivial depois. Só apague quando tiver certeza de que o cliente não vai tentar de novo — mas o motivo não é o link deixar de valer. O token de pareamento é verificado sem consultar o banco (assinatura, tipo e expiração, só isso), então ele continua válido com o placeholder apagado: um cliente que voltar e clicar depois do seu DELETE conecta normalmente, e como a linha original não existe mais, a conclusão cria um número novo — o limite do plano é reavaliado nesse instante e um slot novo é consumido. O risco de apagar cedo demais é ganhar um segundo número inesperado, não revogar o link.

Payload do connect:paired

  • numberId é o id usado em todo o resto da API (GET /v1/numbers/{id}, webhooks, envio).
  • externalRef é o valor que você passou ao gerar o link — cruze com o seu próprio cadastro de cliente. Ele é carregado pelos links Meta; no pareamento por QR chega como null.
  • Todos os seis campos — numberId, phone, displayName, provider, externalRef, redirectUrl — estão sempre presentes: um fluxo que não conhece algum envia null, nunca o omite, então você não precisa de ramificações por fluxo sobre eles.
  • O redirectUrl, quando presente, não é seguido enquanto incorporado — o iframe entrega o valor para você e deixa a sua página decidir se e para onde navegar, em vez de estourar o frame.

White-label / branding

Precedência: parâmetros de query da URL > branding por link (do token gerado, ou capturado como snapshot no número nos links de QR) > branding do tenant (GET/PUT /v1/branding) > padrões da Pilot Status. Os parâmetros de query são apenas cosméticos e deliberadamente restritos — primary, bg, logo, title, subtitle, support, hideBranding. Eles são aplicados somente depois que o token é resolvido como válido; com token inválido ou expirado a página cai nos padrões da Pilot Status e os ignora por completo. O ?logo precisa ser https e é restrito ao host de assets de primeira parte. O estilo do botão e o hideProvenance não fazem parte desse conjunto.

Modelo de segurança

  • O x-api-key permanece no seu backend; somente ele chama POST /v1/numbers/remote-pairing.
  • O token de pareamento autoriza apenas os endpoints de pareamento daquele link e expira — 30 minutos para token Meta, 24 horas para token de pareamento por QR.
  • As origens são validadas em ambos os lados; o iframe roda em connect.*, então suas chamadas de API são same-origin (sem superfície de CORS para o seu app).
  • Tudo que muda a aparência da página para o usuário final é autenticado: vem de um token gerado com a sua chave, nunca de uma URL que um estranho possa digitar.

Relacionados