name: comercialrs-meta-whatsapp-migration
description: Migração do Uazapi para Meta WhatsApp Business API no ComercialRS — estado atual, colunas a criar, credenciais necessárias
ComercialRS — Migração Uazapi para Meta WhatsApp
Diagnóstico — Estado Atual (2026-05-18)
provider atual
- -
automations_config.provider = "uazapi"— funcionando normalmente - - URL:
https://rochasales.uazapi.com - - 8 telefones configurados
- -
/var/www/comercialrs/supabase/functions/dispatch-automation/index.ts—sendViaMeta(),sendToMetaRecipients() - -
/var/www/comercialrs/supabase/functions/send-reminders/index.ts—sendViaMeta() - -
/var/www/comercialrs/supabase/functions/meta-whatsapp-send/index.ts— função específica Meta - -
provider = "meta"✅ - - Phone ID:
812251211981254/+55 11 91508-1701(verificado, ativo) - - WABA ID:
532448808878595— Nome no Meta: "RS - Plano de Saúde" (mesmo cliente, nome diferente) - - WABA Quality: YELLOW, STANDARD throughput
- - Envio direto (type: text) funciona — testado para +55 11 91494-1608 (Julietti)
- - Token de envio (WABA owner):
EAASNGf6ucRcBRYZC7BtZAZBKnRjDTUwkTRpPgRw1jZALy7mQxiUTf6a7zJt7BvLj9iYsht7PtphRfDp9BjgyzjUNmJUru0yTVpzVc2EZCFxbRJID052my8IynikcjOeLAuuuXCFcE9SMKTWEdNUpnS4ltpJK4iD8d3ZAckIZBffhvrBmVFDIkmU3t1eg99klwZDZD - - Token novo (Sales Planos)
EAAq2Dn6CWBU...pertence a Business diferente (118162974594950) — NÃO serve para este WABA - - Token velho (
EAASNGf6ucRc...): System User com acesso ao WABA532448808878595(RS - Plano de Saúde). Pode ENVIAR mensagens mas NÃO pode criar templates. - - Token novo (
EAAq2Dn6CWBU...): pertence ao Business118162974594950(Sales Planos de Saúde). Pode criar templates mas NÃO pode enviar do phone ID812251211981254. - - Para criar templates, o token DEVE pertencer ao Business que é dono da WABA. Gere um novo token a partir do Business que é admin da WABA
532448808878595(não do "Sales Planos de Saúde"). - - Alternativa: criar templates manualmente no Meta Business Manager da WABA.
- - Endpoint:
/functions/v1/test-automation - - Aceita JWT do usuário logado
- - Envia mensagem real via Meta API para o primeiro team_member com telefone
- - Funciona igual ao
test-webhook(ambos usam anon key via Supabase Gateway) - - Separador entre itens:
| - - Emojis de canal: 📹 (video), 📞 (telefone), 💬 (mensagem)
- - Texto fixo final:
- Comercial RS(evita variável no fim) - -
132000— Número de parâmetros não corresponde ao esperado no template - -
132018— Problema com os parâmetros (newline/tab, mais de 4 espaços, etc.) - -
100 (Parameter name is missing)—type: "text"está faltando no parâmetro (sempre incluir) - -
132031— Token sem permissão para enviar this template (verificar token Business) - -
Portuguese (BR)→ código API:pt_BR - -
Portuguese (POR)(europeu) → código API:pt_PT— muito importante! - - Template criado com
Portuguese (POR)só funciona compt_PT, não compt_BR(erro 404) - - Sempre verificar o idioma selecionado no Meta Business Manager ao criar
- - INSTANT com responsible_id →
sendToMetaRecipients()(só o responsável, SEM fallback) - - BROADCAST →
broadcastMetaTemplate()(todos os membros com telefone) - -
sendMetaTemplateagora retorna{ ok: boolean, messageId?: string, error?: string }em vez deboolean - -
logMessageSent()fire-and-forget registra cada envio emmessage_log - -
sendToMetaRecipients()recebe novo parâmetrotaskIde loga após envio - -
GET /functions/v1/meta-webhookcom anon key →UNAUTHORIZED_NO_AUTH_HEADER - -
GET /functions/v1/meta-webhookcom Bearer token →UNAUTHORIZED_LEGACY_JWT(JWT antigo da era v0/v1 não é mais aceito) - - Meta nunca consegue verificar → muestra "Não foi possível validar a URL de callback"
- - Login ComercialRS:
tecrochasales@gmail.com/Rochasales2024 - - Supabase Dashboard:
sales.planoss@gmail.com/tSm?p8n57f+TX5n - - Projeto Supabase:
dauftiqcvgaydddoxqhh - - Anon key:
eyJ_REDACTED - - Supabase Management API Token:
sbp_REDACTED - - Meta User Access Token:
EAASNGf6ucRcBRYZC7BtZAZBKnRjDTUwkTRpPgRw1jZALy7mQxiUTf6a7zJt7BvLj9iYsht7PtphRfDp9BjgyzjUNmJUru0yTVpzVc2EZCFxbRJID052my8IynikcjOeLAuuuXCFcE9SMKTWEdNUpnS4ltpJK4iD8d3ZAckIZBffhvrBmVFDIkmU3t1eg99klwZDZD
Tabela automations_config — colunas existentes
`
id, automation_id, config_key, config_value, created_at, updated_at,
crm_auto_sync, crm_last_sync_count, crm_last_sync_status, crm_last_synced_at,
provider, sales_notification_phones, send_mode,
uazapi_phone, uazapi_token, uazapi_url,
user_id, webhook_mode, webhook_url
`
⚠️ NÃO existem colunas meta_access_token, meta_phone_number_id, meta_waba_id, meta_business_id — precisam ser criadas.
Passo a Passo
1. Criar colunas Meta na tabela
`sql
ALTER TABLE automations_config
ADD COLUMN IF NOT EXISTS meta_access_token TEXT,
ADD COLUMN IF NOT EXISTS meta_phone_number_id TEXT,
ADD COLUMN IF NOT EXISTS meta_waba_id TEXT,
ADD COLUMN IF NOT EXISTS meta_business_id TEXT;
`
2. Obter credenciais do Meta for Developers
| Campo | Onde encontrar | |||
| --- | --- | |||
meta_access_token | Meta for Developers → App → WhatsApp → Configuration → Permanent Token | |||
meta_phone_number_id | Meta for Developers → App → WhatsApp → Phone Numbers → ID da sua linha | |||
meta_waba_id | Meta Business Account → WhatsApp Business → Settings → WABA ID | |||
meta_business_id | Meta Business Account → Settings → Business Info → Business ID | |||
| coluna | tipo | descrição | ||
| -------- | ------ | ----------- | ||
id | uuid | |||
title | text | Título da reunião | ||
due_date | timestamptz | 2026-05-20T14:30 | ||
channel | text | video / telefone / mensagem | ||
link | text | Link da reunião | ||
status | text | todo / in_progress / completed / cancelled | ||
responsible_id | uuid | FK para team_members.id | ||
priority | text | low / medium / high / urgent | ||
deleted_at | timestamptz | Soft delete | ||
| coluna | tipo | descrição | ||
| -------- | ------ | ----------- | ||
id | uuid | |||
key | text | Identificador: task_created, whatsapp_reminders, daily_briefing, risk_alerts, weekly_closing, overdue_alerts | ||
name | text | Nome exibido | ||
description | text | |||
trigger_type | jsonb | Não usado no código | ||
action_type | jsonb | Não usado no código | ||
enabled | boolean | |||
status | text | active / inactive | ||
last_run_at | timestamptz | |||
| coluna | tipo | descrição | ||
| -------- | ------ | ----------- | ||
id | uuid | |||
provider | text | meta ou uazapi | ||
meta_access_token | text | Token Meta | ||
meta_phone_number_id | text | 812251211981254 | ||
meta_waba_id | text | 532448808878595 | ||
meta_business_id | text | 1264409148885415 | ||
uazapi_url | text | |||
uazapi_token | text | |||
uazapi_phone | text | |||
sales_notification_phones | text | phones separadas por vírgula | ||
crm_auto_sync | boolean | |||
send_mode | text | personal | ||
| Nome | Categoria | Params | Body | |
| ------ | ----------- | -------- | ------ | |
task_created | TRANSACTIONAL | 6 | 📹 Nova Reunião Criada\\n📌 {{1}}\\n👤 Responsável: {{2}}\\n📹 {{3}} — {{4}} às {{5}}\\n🔗 {{6}} - ComercialRS | |
meeting_alert_30min | TRANSACTIONAL | 3 | ⏰ Lembrete: Reunião em 30 minutos\\n📌 {{1}}\\n🕐 {{2}}\\n🔗 {{3}} - ComercialRS | |
meetings_overdue_summary | MARKETING | 2 | 🚨 {{1}} reunião(ões) atrasada(s):\n\n{{2}} | |
whatsapp_reminders | TRANSACTIONAL | 2 | 📅 Suas reuniões de hoje ({{1}}):\n\n{{2}} | |
risk_alerts | MARKETING | 2 | ⚠️ Alerta: {{1}} reunião(ões) sem atualização há 3+ dias:\n\n{{2}} | |
daily_briefing | MARKETING | 4 | 📊 Briefing Diário ({{1}})\n\n📅 Reuniões hoje: {{2}}\n✅ Concluídas: {{3}}\n⏳ Pendentes: {{4}} | |
weekly_closing | MARKETING | 1 | 🏁 Encerramento Semanal\n\n✅ {{1}} reunião(ões) encerrada(s) esta semana! | |
task_updated | TRANSACTIONAL | 5 | ✅ Reunião atualizada com sucesso!\n📌 {{1}}\n📅 {{2}} às {{3}}\n📊 Status: {{4}}\n🔗 {{5}} | |
| trigger/key | Quando | Destinatário | Tipo | Template |
| --- | --- | --- | --- | --- |
task_created | Nova reunião criada | responsible_id | INSTANT | nova_reuniao_criada (pt_BR, 6 params) ✅ |
task_updated | Atualização na reunião | responsible_id | INSTANT | reuniao_atualizada_com_sucesso (pt_BR, 5 params) ✅ |
meeting_alert_30min | 30min antes da reunião | responsible_id | INSTANT | lembrete_reuniao_em_30_minutos (pt_BR, 3 params) ⏳ |
meetings_overdue_summary | Tarefas vencidas (instant) | TODOS (broadcast) | INSTANT | 1_reunioes_atrasadas_2 (pt_PT, 2 params) ✅ |
whatsapp_reminders | Reuniões do dia (cron) | TODOS (broadcast) | SCHEDULED | suas_reunioes_de_hoje (pt_PT, 2 params) ✅ |
overdue_alerts | Tarefas vencidas (cron) | TODOS (broadcast) | SCHEDULED | 1_reunioes_atrasadas_2 (pt_PT, 2 params) ✅ |
risk_alerts | 3+ dias sem movimento (cron) | TODOS (broadcast) | SCHEDULED | reunioes_sem_atualizao_ha_3_dias (pt_PT, 2 params) ✅ |
| Funcao | Proposito | Autenticacao | Provider | |
| -------- | ----------- | -------------- | ---------- | |
dispatch-automation | Notificacoes INSTANTANEAS e SCHEDULED | JWT (verify_jwt: true) | meta ou uazapi | |
send-reminders | Cron jobs / resumos agendados | x-cron-secret | meta, uazapi ou n8n | |
test-webhook | Testar conexao do provider (Settings) | anon key | meta ou n8n | |
test-automation | Testar envio real de WhatsApp via automacao | JWT (anon key funciona) | meta |
Fluxo de notificacao:
1. Frontend ou cron dispara dispatch-automation com trigger: "task_created" e payload contendo a task
2. dispatch-automation busca responsible_id da task → encontra phone em team_members
3. Envia via sendToMetaRecipients() para o telefone do responsavel
4. tambem envia fallback para todos com telefone se responsible_id vazio
WhatsApp Bidirecional (2026-05-21)
Problema
Corretores recebem notificação quando reunião é atribuída, mas ao responder o WhatsApp (ex: "iniciada", "concluída") a task não é atualizada no ComercialRS.
Arquitetura implementada
`
Corretor recebe template → responde no WhatsApp → Meta chama meta-webhook
→ parseia resposta → busca task por message_log.meta_message_id (via context.id)
→ valida responsible_id → atualiza tasks.status → confirma via WhatsApp
`
Tabelas criadas (2026-05-21)
`sql
-- LOG de mensagens enviadas (para correlacionar resposta → task)
CREATE TABLE message_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
meta_message_id TEXT UNIQUE, -- wamid.xxx (ID da msg na Meta)
task_id UUID REFERENCES tasks(id),
responsible_id UUID REFERENCES team_members(id),
template_name TEXT,
sent_at TIMESTAMPTZ DEFAULT NOW(),
phone TEXT,
status TEXT DEFAULT 'sent'
);
-- Log de respostas recebidas
CREATE TABLE message_replies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_log_id UUID REFERENCES message_log(id),
meta_message_id TEXT,
from_phone TEXT,
response_text TEXT,
parsed_status TEXT,
task_id UUID REFERENCES tasks(id),
processed_at TIMESTAMPTZ DEFAULT NOW()
);
-- Incoming messages (já existia meta-webhook tentando gravar aqui)
CREATE TABLE incoming_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id TEXT UNIQUE,
from_number TEXT,
timestamp TEXT,
msg_type TEXT,
body TEXT,
source TEXT DEFAULT 'meta_webhook',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Coluna na tasks para guardar o último message_id enviado
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS last_meta_message_id TEXT;
-- Índices
CREATE INDEX idx_message_log_meta_message_id ON message_log(meta_message_id);
CREATE INDEX idx_message_log_task_id ON message_log(task_id);
CREATE INDEX idx_message_log_responsible_id ON message_log(responsible_id);
CREATE INDEX idx_message_replies_message_log_id ON message_replies(message_log_id);
CREATE INDEX idx_message_replies_meta_message_id ON message_replies(meta_message_id);
CREATE INDEX idx_message_replies_task_id ON message_replies(task_id);
CREATE INDEX idx_incoming_messages_message_id ON incoming_messages(message_id);
CREATE INDEX idx_incoming_messages_from_number ON incoming_messages(from_number);
`
RLS policies
`sql
-- message_log: service role full, anon read (para webhook correlacionar)
CREATE POLICY "service_role_full_access_message_log" ON message_log FOR ALL USING (auth.jwt() ->> 'role' = 'service_role');
CREATE POLICY "anon_read_message_log" ON message_log FOR SELECT USING (true);
-- message_replies: service role full, anon insert (webhook recebe replies)
CREATE POLICY "service_role_full_access_message_replies" ON message_replies FOR ALL USING (auth.jwt() ->> 'role' = 'service_role');
CREATE POLICY "anon_insert_message_replies" ON message_replies FOR INSERT WITH CHECK (true);
-- incoming_messages
CREATE POLICY "service_role_full_incoming" ON incoming_messages FOR ALL USING (auth.jwt() ->> 'role' = 'service_role');
CREATE POLICY "anon_insert_incoming" ON incoming_messages FOR INSERT WITH CHECK (true);
-- tasks: service role pode atualizar (webhook precisa atualizar status)
CREATE POLICY "service_role_update_tasks" ON tasks FOR UPDATE USING (auth.jwt() ->> 'role' = 'service_role');
`
Como a resposta é correlacionada
Quando o corretor responde a uma mensagem de template, a Meta envia no webhook:
`json
{
"messages": [{
"context": { "from": "...", "id": "wamid.xxx" }, // ← ID da msg original
"from": "5511914941608",
"text": { "body": "iniciada" }
}]
}
`
O webhook busca message_log WHERE meta_message_id = 'wamid.xxx' → encontra task_id → valida responsible_id → atualiza.
Modificações no dispatch-automation
Deploy
`bash
cd /var/www/comercialrs
SUPABASE_ACCESS_TOKEN=sbp_REDACTED \
supabase functions deploy dispatch-automation --project-ref dauftiqcvgaydddoxqhh
`
Webhook Bidirecional — Como a resposta é correlacionada
Quando o corretor responde a uma mensagem de template, a Meta envia no webhook:
`json
{
"messages": [{
"context": { "from": "...", "id": "wamid.xxx" }, // ← ID da msg original
"from": "5511914941608",
"text": { "body": "iniciada" }
}]
}
`
O webhook busca message_log WHERE meta_message_id = 'wamid.xxx' → encontra task_id → valida responsible_id → atualiza.
⚠️ PROBLEMA CRÍTICO DESCOBERTO: Supabase Edge Functions exige JWT
O endpoint /functions/v1/ do Supabase SEMPRE exige autenticação JWT válida, mesmo para webhooks GET de verificação. Isso impede que a Meta consiga fazer o handshake inicial de verificação.
Sintomas:
Solução implementada (2026-05-21):
1. Servidor Python standalone na porta 8082 (ou 8081 se livre):
- Arquivo: /var/www/comercialrs/data/meta_webhook_server.py
- Sem autenticação JWT — só verifica o hub.verify_token da query string
- Processa POST payloads e atualiza tasks via Supabase REST API com Service Role key
2. Nginx proxy em /etc/nginx/snippets/meta-webhook-proxy.conf:
`nginx
location /meta-webhook/ {
proxy_pass http://127.0.0.1:8082/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_read_timeout 300;
}
`
- Incluir no site Nginx: include /etc/nginx/snippets/meta-webhook-proxy.conf;
3. URL cadastrada na Meta:
- https://comercialrs.rochasalesseguros.com.br/meta-webhook/ (NÃO a /functions/v1/)
4. Service systemd (para manter rodando após reboot):
`ini
[Unit]
Description=Meta WhatsApp Webhook Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/var/www/comercialrs/data
ExecStart=/usr/bin/python3 /var/www/comercialrs/data/meta_webhook_server.py
Restart=always
RestartSec=5
StandardOutput=append:/var/www/comercialrs/data/meta_webhook.log
StandardError=append:/var/www/comercialrs/data/meta_webhook.log
[Install]
WantedBy=multi-user.target
`
`bash
Ativar
systemctl daemon-reload
systemctl enable meta-webhook
systemctl start meta-webhook
Verificar
curl http://127.0.0.1:8082/?hub.mode=subscribe&hub.verify_token=TOKEN&hub.challenge=OK
`
⚠️ PORTA CORRETA: 8082 (NÃO 8083)
Problema comum (2026-05-27): O nginx estava configurado com proxy_pass http://127.0.0.1:8083/ mas o servidor Python ouvinte em 8082 (não 8083). Resultado: todos os webhooks retornavam 502 Bad Gateway e eram perdidos.
Arquivos com a porta no nginx:
`bash
grep -rn '8083' /etc/nginx/
/etc/nginx/snippets/meta-webhook-proxy.conf:5: proxy_pass http://127.0.0.1:8083/;
/etc/nginx/sites-available/comercialrs.rochasalesseguros.com.br:64: proxy_pass http://127.0.0.1:8083/;
`
Correção aplicada (2026-05-27): Ambos mudados para 8082.
Como diagnosticar:
`bash
1. Verificar porta real do servidor Python
ss -tlnp | grep python
2. Testar localmente
curl -X POST http://127.0.0.1:8082/ -d '{"object":"whatsapp_business_account"}' -H 'Content-Type: application/json'
Esperado: 200
3. Verificar nginx logs
tail /var/log/nginx/error.log | grep 502
4. Testar via HTTPS (como Meta vê)
curl -X POST https://comercialrs.rochasalesseguros.com.br/meta-webhook/ -d '{"object":"whatsapp_business_account"}' -H 'Content-Type: application/json'
Esperado: 200
`
Sintoma do problema: Mensagens WhatsApp eram enviadas com sucesso (200 OK do Meta), mas o Chatwoot nunca atualizava. Isso porque o nginx descartava os webhooks de resposta (502) e eles nunca chegavam ao servidor Python.
5. Importante: deploy-vps-meta-1 no Docker é o Postgres-meta (supabase metadata API), NÃO tem nada a ver com WhatsApp. Parar esse container não afeta webhooks.
6. O Edge Function /functions/v1/meta-webhook continua existindo — pode ser útil para testes locais via supabase functions serve, mas NÃO é usado para o webhook real da Meta.
7. Chatwoot não é afetado — usa configuração Nginx separada (não conflita com o snippet de proxy)