Existe uma opção no Embedded Signup do WhatsApp Cloud API que separa os Tech Providers que sabem o que estão fazendo dos que ficam pra trás: a Coexistence.
É aquela linha extra que aparece no popup do Meta — "Conectar um app do WhatsApp Business" — quando o cliente vai vincular o canal. Sem ela, o cliente que já usa o WhatsApp Business no celular precisa abandonar tudo e migrar pra Cloud API pura. Com ela, ele continua usando o app do celular normalmente e o seu sistema espelha as mensagens via webhook.
É o mesmo recurso que o Manychat oferece. E muita gente acha que é exclusivo de Solution Partner — não é. Qualquer Tech Provider verificado pela Meta consegue habilitar.
Este tutorial é o passo a passo prático que usamos pra habilitar Coexistence no funilchatbot.com.br, em produção, sem quebrar o fluxo legado. Tempo total: cerca de 4 horas.
Pré-requisitos (validar antes)
- Tech Provider verificado pela Meta (Access Verification aprovada)
- App Review aprovado com duas permissões em Advanced Access:
whatsapp_business_messagingewhatsapp_business_management - Business verification completo no Business Manager
- App em modo Live (não Development)
- Embedded Signup Configuration criada e em uso
- O cliente final precisa ter o WhatsApp Business app no celular versão 2.24.17 ou superior
Se o App Review ainda não foi aprovado, Coexistence não funciona. Resolva isso primeiro — sem Advanced Access nas duas permissões, o popup do Meta nem oferece a opção.
Parte 1 — Configurar o painel Meta (cerca de 10 minutos)
1.1. Habilitar Coexistence no Embedded Signup Config
Acesse developers.facebook.com/apps/<APP_ID>/use_cases/ e siga:
- Casos de uso → clique em "Personalizar" no card "Conectar-se com clientes pelo WhatsApp"
- Menu lateral → Configurador de cadastro incorporado
- Role até a seção Diálogo do cadastro incorporado
- No dropdown Tipo de recurso · Opcional, selecione: Integração do app WhatsApp Business
- Confirme que a Versão do cadastro incorporado está em v4 e a Versão das informações da sessão em 3
1.2. Assinar 3 webhooks adicionais
No painel developers.facebook.com/apps/<APP_ID>/dashboard/, vá ao produto WhatsApp → Configuração. Na seção de webhooks da sua WABA, assine (além de messages que você já tem):
history— histórico de mensagens passadas (até 180 dias)smb_app_state_sync— sincronização de contatos do clientesmb_message_echoes— mensagens que o cliente envia pelo app do celular
Importante: esses três webhooks precisam estar assinados no app E no subscribed_apps da WABA. O painel cobre o primeiro lado; o segundo lado é configurado via código (item 2.4 abaixo).
Parte 2 — Implementação no código
2.1. Frontend: alterar o FB.login
A mudança crítica é uma só. Antes:
FB.login(callback, {
config_id: '<SEU_CONFIG_ID>',
response_type: 'code',
override_default_response_type: true,
})
Depois:
FB.login(callback, {
config_id: '<SEU_CONFIG_ID>',
response_type: 'code',
override_default_response_type: true,
extras: {
setup: {},
featureType: 'whatsapp_business_app_onboarding',
sessionInfoVersion: '3',
},
})
É o featureType: 'whatsapp_business_app_onboarding' que ativa a Coexistence. Sem ele, o popup do Meta nem oferece a opção. Com ele, a opção "Conectar um app do WhatsApp Business" aparece automaticamente no dropdown de seleção de WABA.
2.2. Frontend: listener postMessage
O evento retornado pelo Meta é diferente. No fluxo padrão você ouve FINISH. Em Coexistence, é FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING. Adapte o listener:
window.addEventListener('message', (event) => {
if (!event.origin || !/^https:\/\/(.*\.)?facebook\.com$/.test(event.origin)) return
let data = event.data
if (typeof data === 'string') {
try { data = JSON.parse(data) } catch { return }
}
if (data?.type !== 'WA_EMBEDDED_SIGNUP') return
const { event: evt, data: payload } = data
if (evt === 'FINISH' || evt === 'success') {
handleStandardFlow(payload)
return
}
if (evt === 'FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING') {
handleCoexistenceFlow({ ...payload, coexistence: true })
return
}
if (evt === 'CANCEL') {
console.warn('signup cancelado:', payload)
}
})
O payload do FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING traz os mesmos campos do FINISH normal: waba_id, phone_number_id, business_id. Você só precisa marcar internamente que aquela conexão é Coexistence pra rotear no backend.
2.3. Backend: aceitar WABA do cliente no auto-discovery
Aqui está um gotcha que pega muita gente. Em Coexistence, a WABA pertence ao cliente, não ao seu Business Manager. Ela aparece em client_whatsapp_business_accounts, não em owned_whatsapp_business_accounts.
Se o seu auto-discovery só olha em owned_* (como o nosso olhava), ele vai retornar "WABA não encontrada" e o fluxo trava. Adicione o fallback:
const meR = await fetch(`${GRAPH}/me/businesses`, {
headers: { Authorization: `Bearer ${userToken}` }
})
const bms = (await meR.json())?.data || []
for (const bm of bms) {
// Fluxo padrão: WABA própria
const ownR = await fetch(`${GRAPH}/${bm.id}/owned_whatsapp_business_accounts`, {
headers: { Authorization: `Bearer ${userToken}` }
})
const owned = await ownR.json()
if (owned?.data?.length) {
wabaId = owned.data[0].id
break
}
// Fallback Coexistence: WABA do cliente compartilhada
const cliR = await fetch(`${GRAPH}/${bm.id}/client_whatsapp_business_accounts`, {
headers: { Authorization: `Bearer ${userToken}` }
})
const client = await cliR.json()
if (client?.data?.length) {
wabaId = client.data[0].id
break
}
}
2.4. Backend: subscribe app com fields de Coexistence
Quando você chama POST /<WABA_ID>/subscribed_apps com body vazio, a Meta assina só os defaults — basicamente messages. Os três webhooks de Coexistence ficam de fora.
Mude para passar a lista explícita, condicional ao tipo de conexão:
const subscribedFields = coexistence
? [
'messages',
'message_template_status_update',
'phone_number_quality_update',
'message_echoes',
'smb_app_state_sync',
'history',
]
: [
'messages',
'message_template_status_update',
'phone_number_quality_update',
]
await fetch(`${GRAPH}/${wabaId}/subscribed_apps`, {
method: 'POST',
headers: {
Authorization: `Bearer ${userToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ subscribed_fields: subscribedFields }),
})
2.5. Backend: pular o POST /register em Coexistence
No fluxo Cloud API padrão você chama POST /<PHONE_NUMBER_ID>/register com messaging_product: 'whatsapp' e um PIN. Em Coexistence, esse número já está registrado no app do celular do cliente. Se você chamar register, a Meta retorna erro.
if (!coexistence) {
try {
await fetch(`${GRAPH}/${phoneNumberId}/register`, {
method: 'POST',
headers: {
Authorization: `Bearer ${userToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
messaging_product: 'whatsapp',
pin: '000000',
}),
})
} catch (e) { /* idempotente — silent fail */ }
}
2.6. Webhook handler: tratar os 3 fields novos
Aqui está a parte mais crítica e que mais quebra se feita errado. Os três webhooks novos (message_echoes, smb_app_state_sync, history) NÃO devem disparar a IA.
Se você só remove o filtro if (change.field !== 'messages') continue sem adicionar handlers específicos, vai acontecer o seguinte:
- O dono do número digita uma mensagem no app do celular → vira um echo → seu sistema interpreta como mensagem do cliente → IA responde para o próprio dono. Constrangedor.
- O histórico chega com 180 dias de mensagens antigas → IA tenta responder cada uma em loop → você queima crédito Anthropic e o cliente recebe 180 mensagens da sua IA.
Trate cada field separadamente:
async function handleMetaWebhook(payload) {
for (const entry of payload.entry || []) {
for (const change of entry.changes || []) {
if (change.field === 'message_echoes' || change.field === 'smb_message_echoes') {
await handleEcho(change.value || {})
continue
}
if (change.field === 'history') {
await handleHistoryBackfill(change.value || {})
continue
}
if (change.field === 'smb_app_state_sync') {
// contatos sincronizados — log/sync conforme necessário
continue
}
if (change.field !== 'messages') continue
// handler de inbound normal — inalterado
}
}
}
async function handleEcho(value) {
// Salva como outbound, marca conversa como atendida manualmente.
// NUNCA chama runAIAgent aqui.
}
async function handleHistoryBackfill(value) {
// Importa mensagens com metadata.source='coexistence_history_backfill'.
// NUNCA chama runAIAgent aqui.
}
Adicione idempotência em messages também — Coexistence reenvia mensagens em retry mais agressivamente que o fluxo padrão. Um ON CONFLICT (workspace_id, whatsapp_msg_id) DO NOTHING resolve.
Parte 3 — Banco de dados (recomendado)
Adicione uma flag pra identificar canais Coexistence no seu sistema. Você vai precisar dela pra mostrar badge na UI, lógica de envio diferente (throughput limitado, ver limitações abaixo) e relatórios.
ALTER TABLE channels ADD COLUMN IF NOT EXISTS coexistence_mode BOOLEAN DEFAULT false;
ALTER TABLE channels ADD COLUMN IF NOT EXISTS created_via TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS channels_workspace_waba_uniq
ON channels(workspace_id, waba_id) WHERE waba_id IS NOT NULL;
No INSERT do canal, grave coexistence_mode=true e created_via='embedded_signup_coexistence' quando o fluxo for Coexistence.
Parte 4 — Envio de mídia (gotcha que muita gente esquece)
A Cloud API NÃO aceita base64 em image.link, audio.link ou video.link. Só URL HTTP/HTTPS pública ou media_id obtido via upload.
Se o seu inbox manda mídia hoje como data:image/jpeg;base64,..., a Meta rejeita silenciosamente. Funciona em provider não-oficial como Evolution/Baileys (que aceita base64), mas em Cloud API não.
Duas soluções:
- Faça upload da mídia no seu S3/MinIO próprio primeiro, gera uma URL pública, passa essa URL para a Cloud API.
- Faça upload pra Meta antes:
POST /<PHONE_NUMBER_ID>/mediavia multipart, recebe ummedia_id, usaimage.idem vez deimage.link.
A primeira opção é mais simples se você já tem MinIO/S3 servido por proxy reverso (Caddy, Nginx, Cloudflare). A segunda é mais robusta porque a Meta cacheia a mídia e você não depende da sua infra estar de pé.
Validação E2E
Depois de tudo isso, o teste de validação é direto:
- Cliente abre o seu sistema → tela de "Conectar WhatsApp"
- Clica "Conectar com Facebook" → popup do Meta abre
- No dropdown "Conta do WhatsApp Business", deve aparecer a opção "Conectar um app do WhatsApp Business" entre as outras. Se aparecer, Coexistence está ativo.
- Cliente escolhe essa opção → autoriza no celular dele (recebe push notification do WhatsApp Business)
- O callback retorna o evento
FINISH_WHATSAPP_BUSINESS_APP_ONBOARDINGno listener - Backend cria o channel com
coexistence_mode=true
Cenários para testar:
- Manda mensagem pro número do cliente → chega no seu inbox (handler
messages) - Cliente responde pelo celular → chega como echo (handler
message_echoes) → sua IA NÃO responde, conversa marca como atendida - Cliente responde pelo seu inbox → mensagem sai e aparece no celular do cliente também
Limitações documentadas pela Meta
Documente essas limitações no onboarding do cliente. Cliente que precisa de mais de 20 msg/s vai precisar migrar pra Cloud API pura (sem Coexistence). Cliente que usa muito grupo vai descobrir do jeito errado se você não avisar.
Deadline crítico: 15 de outubro de 2026
A Meta vai descontinuar Embedded Signup v2 e v3 nessa data. O featureType: 'coex' antigo já está deprecado — o nome novo é whatsapp_business_app_onboarding (v4), que é o que este tutorial usa.
Mas se você ainda usa outros featureType da v3 (only_waba_sharing, marketing_messages_lite), eles vão quebrar em outubro. Recomendação prática: planeje a migração v4 completa até agosto de 2026 pra ter buffer pra debug e teste com clientes reais.
Conclusão
Coexistence não é mágica nem privilégio de Solution Partner. É uma feature documentada do Embedded Signup que qualquer Tech Provider verificado consegue habilitar — desde que tenha App Review aprovado com as duas permissões em Advanced Access.
O trabalho prático é cirúrgico: uma config no painel, três webhooks adicionais assinados, uma linha alterada no FB.login, um fallback no auto-discovery, três handlers novos no webhook receiver, idempotência em mensagens, e os ajustes de mídia que mostramos.
O cliente final ganha um onboarding muito mais suave (continua usando o app que ele já conhece) e você ganha um diferencial competitivo que o Manychat e a maioria dos concorrentes brasileiros ainda não oferecem. Vale o trabalho.