name: comercialrs-whatsapp-webhook-vps
description: Webhook bidirecional WhatsApp no VPS — recebe respostas de corretores e atualiza tasks via psycopg2 direto
category: devops
ComercialRS — Webhook Bidirecional WhatsApp (VPS)
Contexto
Quando um corretor recebe notificação WhatsApp e responde ("iniciada", "concluida", "remarketing"), o servidor na VPS deve:
1. Receber o webhook da Meta
2. Buscar a task pelo meta_message_id (wamid) em auth.message_log
3. Atualizar auth.tasks.status
4. Enviar confirmação ao corretor via Meta Send API
Arquitetura
- - Servidor: Python na VPS (
/var/www/comercialrs/data/meta_webhook_vps.py), porta 8083 - - Proxy Nginx:
/meta-webhook/→127.0.0.1:8083(viameta-webhook-proxy.conf) - - Banco: Supabase Cloud via psycopg2 —
host=db.dauftiqcvgaydddoxqhh.supabase.co, schemapublic(NAOauth) - - Credentials:
user=postgres, password=tSm?p8n57f+TX5n(do Supabase Dashboard → Project Settings → Database) - - Conexão: usa psycopg2 direto, NÃO PostgREST
- -
public.message_log—meta_message_id,task_id,phone,responsible_id - -
public.tasks—id,status,title - -
public.incoming_messages— logging de mensagens recebidas (msg_id PK, timestamps sao bigint) - -
public.team_members—id,phone(phone sem country code, ex:11913412661) - -
public.automations_config—meta_access_token,meta_phone_number_id - - App
1281042693386519→ webhook emn8n.primeflowdigital.com.br/webhook/...(n8n automation) - - App
3014923135375381→ webhook emcomercialrs.rochasalesseguros.com.br/meta-webhook/(VPS Python direto) - -
in_progress→ status "Iniciada" - -
completed→ status "Concluida" - -
remarketing→ status "Remarketing"
Tabelas envolvidas (schema: public — NAO auth)
Colunas do token Meta
CRÍTICO: automations_config só tem campos Uazapi (uazapi_url, uazapi_token, etc.). O token da Meta (EAAK...) e phone_number_id (578716225306497) NÃO estão no banco local.
Opções para guardar:
1. Adicionar meta_access_token TEXT e meta_phone_number_id TEXT em auth.automations_config (recomendado)
2. Criar tabela auth.meta_config separada
3. Variáveis fixas no Python (menos seguro)
Status keywords
`python
STATUS_KEYWORDS = {
'in_progress': ['iniciada', 'comecou', 'começou', 'andamento', 'em andamento', 'start', 'started'],
'completed': ['concluida', 'concluída', 'finalizada', 'done', 'completo', 'completa', 'fechada'],
'remarketing': ['remarketing', 'remar', 'rmk', 'remercar']
}
`
Correlação via context.message_id
Quando o corretor responde a uma mensagem enviada, o webhook inclui context.id = wamid original.
Esse wamid = meta_message_id em auth.message_log → lookup direto → task_id.
Deploy/restart
`bash
Copiar arquivo atualizado
sshpass -p 'Marcia19671951@' scp meta_webhook_vps.py root@31.97.243.106:/var/www/comercialrs/data/
Reiniciar servidor (porta 8083)
ssh root@31.97.243.106 "fuser -k 8083/tcp 2>/dev/null; sleep 1; cd /var/www/comercialrs/data && nohup python3 -u meta_webhook_vps.py > meta_webhook_vps.log 2>&1 &"
Ver logs
ssh root@31.97.243.106 "tail -20 /var/www/comercialrs/data/meta_webhook_vps.log"
`
Verificar tokens no banco
`bash
docker exec deploy-vps-db-1 psql -U postgres -d postgres -c "SELECT column_name FROM information_schema.columns WHERE table_schema='auth' AND table_name='automations_config'"
`
CRÍTICO: Schema public (NAO auth)
Todas as tabelas estao no schema public do Supabase Cloud — NAO auth. Queries com auth. retornam linhas vazias sem erro visivel.
Dois Apps Meta (N8N vs VPS)
O ComercialRS usa dois apps Meta Business separados — NÃO misturar:
Cada app tem seu próprio webhook URL, token de verificação e access token. Não são intercambiáveis.
Cronologia da descoberta (trial-and-error)
1. Botão clicado → webhook recebe → find_task_by_phone_and_status retorna None → HTTP 200 nunca enviado → curl timeout
2. Descoberto que Postgres local (172.23.0.2) tem apenas 1 team_member vs Supabase Cloud tem 5+
3. Schema era auth em vez de public — queries falhavam silenciosamente
4. O incoming_messages tem timestamp como bigint — passar '' causa invalid input syntax for type bigint
5. corrigido: DB_HOST=db.dauftiqcvgaydddoxqhh.supabase.co, schema public, timestamp como 0 ou omitido
6. Log de webhook NÃO mostra botões — o log só registra POST requests brutos; o parse de button.payload ocorre em memória mas não era written to log. Porém incoming_messages table é o source-of-truth for button clicks.
7. Confirmado: cliques de botão geram incoming_messages rows com parsed_status preenchido (ex: in_progress) e body = texto do botão ("Iniciada"). Tasks correspondentes têm updated_at correlacionado (mesmo timestamp em segundos).
Phone cleanup no lookup
`python
import re
phone = '5511913412661'
phone_clean = re.sub(r'\D', '', phone).lstrip('55')
Resultado: '11913412661' — mapeia para team_members.phone sem country code
`
Button payloads (custom type)
Workflow button click (sem context.id)
`python
1. Extrair phone do message.from (5511913412661)
2. phone_cleanup → 11913412661
3. SELECT id FROM public.team_members WHERE phone ILIKE '%11913412661%'
4. SELECT id FROM public.tasks WHERE responsible_id=X AND status IN ('todo','in_progress') ORDER BY created_at DESC LIMIT 1
5. UPDATE status via RPC
6. send_meta_reply(to=5511913412661, text="Reuniao atualizada para X")
`
Colunas do token Meta
automations_config tem meta_access_token e meta_phone_number_id — confirmados no Supabase Cloud.
Deploy/restart
`bash
sshpass -p 'Marcia19671951@' ssh root@31.97.243.106 "fuser -k 8083/tcp 2>/dev/null; sleep 1; nohup python3 /var/www/comercialrs/data/meta_webhook_vps.py &>/var/log/meta_webhook.log &"
sleep 2
sshpass -p 'Marcia19671951@' ssh root@31.97.243.106 "tail /var/log/meta_webhook.log"
`
Auditar fluxo completo (WhatsApp → Kanban)
`bash
1. Quantas tarefas por status?
curl -s ".../rest/v1/tasks?select=status&limit=500" | \
python3 -c "import sys,json; from collections import Counter; print(Counter(t['status'] for t in json.load(sys.stdin)))"
2. Cliques de botão mais recentes (quem clicou, quando, qual status)
curl -s ".../rest/v1/incoming_messages?select=body,parsed_status,task_id,created_at,from_number&order=created_at.desc&limit=20" | \
python3 -m json.tool
3. Tasks que foram atualizadas HOJE (status change, não só new tasks)
curl -s ".../rest/v1/tasks?select=id,status,updated_at&updated_at=gte.2026-06-01T00:00:00Z&order=updated_at.desc&limit=20" | \
python3 -m json.tool
4. Cross-ref: incoming_messages clicadas HOJE vs tasks atualizadas HOJE
Se task_id do clique aparece em updated_at hoje E o status = parsed_status → funcionando
`
Problemas conhecidos
1. PostgREST SSL — localhost:3000 dá [SSL: WRONG_VERSION_NUMBER] quando chamado da VPS. Solução: usar psycopg2 direto.
2. automations_config sem token Meta — meta_access_token não existe nessa tabela. Criar colunas ou tabela separada.
3. Meta webhook verification — se o SSL do Kong for autoassinado, a Meta não valida. Curl funciona com -k mas Meta não. Causa: ERR_CONNECTION_RESET. Solução: Let's Encrypt no Kong.
4. Dois servidores — às vezes o servidor velho (8082) e novo (8083) rodam juntos. Matar o da 8082 se não for mais usado.
5. HTTP 200 não enviado em exceção — se update_task_status_rpc levanta exceção sem try/except, wfile.write() nunca é chamado → curl timeout 10s. Sempre usar try/except e garantir HTTP response.
6. Timestamp bigint no incoming_messages — o campo timestamp é bigint, nao text. Passar '' causa invalid input syntax for type bigint. Passar '0' ou omitir o campo.
7. Logging para stdout no nohup — print() nao vai para /var/log/meta_webhook.log no nohup. Usar logging module com filename='/var/log/meta_webhook.log'.
Verificar cliques de botão (source of truth = incoming_messages table)
O log em /var/www/comercialrs/data/meta_webhook_vps.log pode mostrar só "POST /" sem [button]
— isso NÃO significa que os botões não funcionam. O log gravava só POST sem parsear o payload.
Verificar via banco (supabase REST API):
`bash
Listar cliques de botão mais recentes
curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/incoming_messages?select=body,parsed_status,task_id,created_at&order=created_at.desc&limit=50" \
-H "apikey: OtwMAcrs2L3iH60ppShVTrOuQYSolIwITUPVf1ckswk" \
-H "Authorization: Bearer OtwMAcrs2L3iH60ppShVTrOuQYSolIwITUPVf1ckswk"
Contar por status
curl -s ".../incoming_messages?select=parsed_status&limit=500&offset=0" | \
python3 -c "import sys,json; data=json.load(sys.stdin); from collections import Counter; print(Counter(d['parsed_status'] for d in data))"
`
Confirmar que task foi atualizada após clique:
`sql
-- O clique gera incoming_messages.parsed_status='in_progress' E tasks.updated_at=timestamp_do_clique
SELECT t.id, t.status, t.updated_at, im.created_at as clique_time
FROM public.tasks t
JOIN public.incoming_messages im ON im.task_id = t.id
WHERE im.parsed_status IS NOT NULL
ORDER BY im.created_at DESC LIMIT 10;
`
Teste manual
`bash
GET verification
curl "http://31.97.243.106:8083/?hub.mode=subscribe&hub.verify_token=1c526598c56179f772be896d233badab&hub.challenge=TEST"
POST simulando resposta
curl -X POST http://31.97.243.106:8083/ \
-H "Content-Type: application/json" \
-d '{"object":"whatsapp_business_account","entry":[{"changes":[{"value":{"metadata":{"phone_number_id":"578716225306497"},"messages":[{"id":"wamid.DEBUG_ORIG","from":"5511945678901","text":{"body":"iniciada"},"context":{"id":"wamid.DEBUG_ORIG"}}]}}]}]}'
`