name: comercialrs-whatsapp-spam-block
description: Investigate and prevent WhatsApp Business spam blocks on ComercialRS Uazapi integration
ComercialRS — WhatsApp Spam Block Analysis
Context
WhatsApp Business number blocked for 5 hours (anti-spam). Goal: understand the root cause by analyzing the message sending architecture.
Key Findings
Configuração Uazapi (from automations_config table)
`
uazapi_url: https://rochasales.uazapi.com
uazapi_token: c2d9806f-b02d-4a1e-acb6-a51db076c4a5
uazapi_phone: 11913412661
send_mode: personal
`
COMO CONFIGURAR O WEBHOOK DO UAZAPI (via API)
⚠️ NÃO confunda com Chatwoot — Uazapi e Chatwoot são sistemas SEPARADOS:
- - Uazapi = conexão direta com WhatsApp (envio/recebimento de mensagens)
- - Chatwoot = plataforma de atendimento (não é usada neste fluxo)
- -
excludeMessages: ["fromMe"]— NÃO processa mensagens enviadas pelo próprio sistema (crucial!) - -
events: ["messages"]— recebe todas as mensagens do WhatsApp - - URL deve apontar para a Edge Function
uazapi-webhookno Supabase - -
/var/www/comercialrs/supabase/functions/dispatch-automation/index.ts— envio de alertas - -
/var/www/comercialrs/supabase/functions/test-broadcast/index.ts— teste de broadcast - -
/var/www/comercialrs/supabase/functions/uazapi-webhook/index.ts— webhook receptor (719 linhas, parseia respostas) - -
/var/www/comercialrs/supabase/functions/register-uazapi-webhook/index.ts— registra webhook no Uazapi - -
/var/www/comercialrs/.env— contém anon key (não service role) - -
alert_deliveries— log de envios (event_type, target_phone, task_ids, signature) - -
webhook_health— status do webhook receptor - -
tasks— reuniões (status, responsible_id, external_ref) - - Implementar delay entre envios (1-2s entre cada)
- - Não enviar para números que não responderam nos últimos 7 dias
- - Usar
send_mode: 'personal'ao invés de broadcast em massa - - Implementar rate limiting: máx 5-10 mensagens por minuto
- - Throttle por assinatura (responsável + evento) — não reenvia se já alertou nos últimos 30 min
- - Assinatura =
${event_type}:${responsible_id}— identifica cada combinação única responsável/evento - - Janela = 30 minutos (configurável via
windowMs) - - Checagem:
checkThrottle()→ busca se existe registro com mesma signature criado nos últimos 30 min - - Registro:
recordThrottle()→ insere após cada envio bem-sucedido - - Aplicado em:
overdue_alerts(linha 549-575) erisk_alerts(linha 597-624) dodispatch-automation - -
webhook_health— última mensagem recebida (em vez dewebhook_logs) - -
alert_deliveries— log de envios (event_type, target_phone, task_ids, signature) - -
tasks— reuniões (status, responsible_id, title, link) - -
team_members— membros (id, name, phone)
Endpoint: POST https://rochasales.uazapi.com/webhook
Header: token: c2d9806f-b02d-4a1e-acb6-a51db076c4a5
Verificar configuração atual:
`bash
curl -s "https://rochasales.uazapi.com/webhook" \
-H "token: c2d9806f-b02d-4a1e-acb6-a51db076c4a5"
Retorna: [{"enabled":false,"events":[],"id":"r2483a9bc67c89a","url":""}]
`
Ativar/configurar o webhook:
`bash
curl -s -X POST "https://rochasales.uazapi.com/webhook" \
-H "Content-Type: application/json" \
-H "token: c2d9806f-b02d-4a1e-acb6-a51db076c4a5" \
-d '{
"url": "https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/uazapi-webhook",
"enabled": true,
"events": ["messages"],
"excludeMessages": ["fromMe"],
"addUrlEvents": false,
"addUrlTypesMessages": false
}'
`
Configuração餈蛛 importante:
Fluxo completo que funciona:
1. Cria tarefa no banco (status: todo, responsible_id: ID do membro)
2. Sistema envia notificação via Uazapi /send/text
3. Membro responde no WhatsApp com "iniciada", "concluido" ou "remarketing"
4. Uazapi reencaminha a mensagem para o webhook
5. Edge function identifica a tarefa pelo título na mensagem
6. Atualiza status no banco e envia confirmação de volta automaticamente
Testado e funcionando em 08/05/2026 — todas as confirmações foram enviadas corretamente.
Como o sistema envia mensagens
Edge function test-broadcast e dispatch-automation usam:
`typescript
POST https://rochasales.uazapi.com/send/text
Headers: { "Content-Type": "application/json", "token": token }
Body: { "number": "55119...", "text": "🔴 Alert message..." }
`
Causa provável do bloqueio por spam
WhatsApp Business bloqueia números quando:
1. Envio para números sem resposta — o sistema manda alertas para membros mesmo quando eles não interagem. WhatsApp detecta "envio sem reply" como spam.
2. Volume alto em sequência — alertas de reuniões atrasadas são enviados para TODOS os membros simultaneamente. Se são 10+ membros = 10+ chamadas API em poucos segundos.
3. Sem período de aquecimento — número novo enviando muitas mensagens rapidamente.
Arquivos importantes no servidor
Query para ver configuração
`sql
SELECT * FROM automations_config LIMIT 1;
-- key: uazapi_url, uazapi_token, uazapi_phone, send_mode
`
Tabelas relacionadas
How to Investigate
1. Query alert_deliveries para ver últimos envios: created_at DESC, event_type, target_phone
2. Verificar webhook_health.last_received_at — última resposta recebida
3. Analisar dispatch-automation e test-broadcast para entender volume de envios
4. Checar se send_mode: 'personal' ou 'broadcast' — broadcast em grupo é mais susceptível a spam
Prevention
✅ Throttle de Spam — alert_throttle (IMPLEMENTADO em 26/05/2026)
Tabela a criar no Supabase Dashboard (SQL Editor):
`sql
CREATE TABLE IF NOT EXISTS alert_throttle (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
signature TEXT UNIQUE NOT NULL, -- hash: evento:responsible_id
event_type TEXT NOT NULL, -- ex: overdue_alerts, risk_alerts
target_phone TEXT,
responsible_id UUID,
task_ids TEXT[],
created_at TIMESTAMPTZ DEFAULT now(),
expires_at TIMESTAMPTZ DEFAULT now() + '30 minutes'
);
CREATE INDEX IF NOT EXISTS idx_alert_throttle_signature_created
ON alert_throttle(signature, created_at DESC);
`
Como funciona:
Deploy da função:
`bash
cd /var/www/comercialrs && supabase functions deploy dispatch-automation --project-ref dauftiqcvgaydddoxqhh
`
Log de throttle (verifica se está funcionando):
`bash
Procura logs com "throttled" no deployment mais recente
O dispatch-automation logs no console quando pula: "⏭️ overdue_alerts → {rid} (throttled, já alertado há <30min)"
`
⚠️ VERIFICAÇÃO OBRIGATÓRIA DO THROTTLE
After deploying throttle changes, the throttle CAN silently fail. The function's console.log shows "throttled" but errors are swallowed by .catch().
Checklist:
1. Table has RLS status verified:
`sql
SELECT tablename, rowsecurity FROM pg_tables WHERE tablename = 'alert_throttle';
-- If rowsecurity=true: throttle writes get silently dropped. Fix:
ALTER TABLE alert_throttle DISABLE ROW LEVEL SECURITY;
`
2. Write test — insert a record and READ IT BACK:
`sql
INSERT INTO alert_throttle (id, signature, event_type, responsible_id, task_ids, expires_at)
VALUES (gen_random_uuid(), 'test:debug', 'test_event', gen_random_uuid(), '{}', now() + interval '30 minutes');
SELECT * FROM alert_throttle WHERE signature = 'test:debug'; -- must return 1 row
DELETE FROM alert_throttle WHERE signature = 'test:debug';
`
3. Verify service_key can read/write via REST API — checkThrottle() and recordThrottle() use the REST API (/rest/v1/alert_throttle) not direct Postgres. If RLS blocks, reads return [] and inserts silently fail.
4. Check alert_throttle table for recent records after next automation run:
`sql
SELECT signature, event_type, created_at, expires_at
FROM alert_throttle
ORDER BY created_at DESC LIMIT 10;
-- If empty after overdue_alerts ran → throttle not writing
`
⚡ Anti-Spam: Duplicate Notification Block via message_log
Problema: whatsapp_reminders cron busca tasks com due_date = hoje e status = todo, enviando nova_reuniao_criada para cada uma — mesmo que já tenha sido enviada antes (ex: via task_created instantâneo). Cada execução do cron reenvia para todas as tasks, causando spam.
Solução: Verificar message_log antes de enviar. Se já existe registro de envio do mesmo template para a mesma task, pular.
Função adicionada em dispatch-automation/index.ts:
`typescript
async function wasTaskAlreadyNotified(
serviceKey: string,
taskId: string,
templateName: string
): Promise
try {
const rows = await fetchJSON(
${supabaseUrl}/rest/v1/message_log?task_id=eq.${taskId}&template_name=eq.${templateName}&status=eq.sent&select=id,
{ headers: { 'Authorization': Bearer ${serviceKey}, 'apikey': serviceKey } }
) as { id: string }[];
return rows.length > 0;
} catch {
return false;
}
}
`
Uso em whatsapp_reminders (antes de cada envio):
`typescript
const alreadySent = await wasTaskAlreadyNotified(serviceKey, task.id, 'nova_reuniao_criada');
if (alreadySent) {
console.log(⏭️ whatsapp_reminders → task=${task.id} (${task.title}): ja notificado, pulando);
continue;
}
`
Importante: O message_log é gravado após cada envio bem-sucedido via logMessageSent(). Cada task+template único gera um registro. Isso protege contra duplicação tanto do instantâneo (task_created) quanto do cron.
Automations — Sources of Spam (Análise 26/05/2026)
| Automação | Gatilho | Anti-spam atual | Risco |
| --- | --- | --- | --- |
whatsapp_reminders | cron 15min | ✅ message_log (agora) | Spam por reenvio de tasks já notificadas |
overdue_alerts | cron diário | ✅ alert_throttle 30min | OK |
risk_alerts | cron diário | ✅ alert_throttle 30min | OK |
task_created | instantâneo | ❌ | Potencial duplicado se cron também enviar |
task_updated | instantâneo | ❌ | Pode enviar junto com cron (templates diferentes) |
Recomendação: Adicionar throttle também para task_created e task_updated (janela 30min por task). Currently, if a task is created and edited multiple times, each action could trigger an immediate WhatsApp.
Como Testar o Fluxo de Resposta do WhatsApp (Uazapi Webhook)
⚠️ Erro comum: "no_tasks" na resposta
Se o webhook retorna {"skipped": true, "reason": "no_tasks"} — significa que o sistema recebeu a mensagem mas não encontrou tarefas "todo" linked ao número que respondeu. As tarefas precisam existir no banco ANTES de enviar a notificação.
Passo a passo para teste real:
1. Verificar se o webhook está registrado no Uazapi:
`bash
curl -s "https://rochasales.uazapi.com/webhook" \
-H "Authorization: Bearer c2d9806f-b02d-4a1e-acb6-a51db076c4a5"
Deve retornar {"addUrlEvents":true, "enabled":true, ...}
`
2. Encontrar o phone_id do responsável no banco:
`bash
curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/team_members?select=id,name,phone" \
-H "apikey: ANON_KEY" | python3 -m json.tool
`
3. Criar tarefa de teste no banco (via Supabase Dashboard → Table Editor → tasks):
`
title: TESTE - Reunião Follow-up
status: todo
responsible_id:
due_date: 2026-05-08T14:00
channel: mensagem
link: https://app2.paineldocorretor.com.br/crm/negocios/teste001
`
4. Enviar notificação via API Uazapi:
`bash
curl -s -X POST "https://rochasales.uazapi.com/send/text" \
-H "Content-Type: application/json" \
-H "token: c2d9806f-b02d-4a1e-acb6-a51db076c4a5" \
-d '{
"number": "11913412661",
"text": "📋 Nova Reunião\n\n• Título: TESTE - Reunião Follow-up\n• Responsável: Davi\n• Canal: 💬 Mensagem\n• Horário: 08/05/2026 às 14h00\n\n_Para atualizar o status, responda com: Iniciada | Concluído | Remarketing_"
}'
`
5. Responsável responde no WhatsApp com "iniciada", "concluido" ou "remarketing".
6. Verificar se chegou resposta:
`bash
Log do webhook (tabela webhook_health)
curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/webhook_health?select=*" \
-H "apikey: ANON_KEY"
`
Como o webhook identifica a tarefa:
1. Extrai o telefone do remetente
2. Busca team_members por telefone → encontra responsible_id
3. Busca tarefas "todo" do responsável (status=eq.todo, responsible_id=eq.X)
4. Faz matching pelo título da reunião que aparece na mensagem
5. Se encontra → atualiza status e envia confirmação de volta
6. Se não encontra → retorna "reason":"no_tasks" (sem confirmação)
Formato do payload que o Uazapi envia para o webhook:
`json
{
"event": "messages",
"data": {
"from": "5511913412661@c.us",
"fromMe": false,
"text": "concluido"
},
"message": {
"fromMe": false,
"chatid": "5511913412661@c.us"
}
}
`
Testar o webhook manualmente:
`bash
curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/uazapi-webhook" \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ANON_KEY" \
-d '{"data":{"text":"concluido","from":"5511913412661@c.us","fromMe":false},"message":{"fromMe":false,"chatid":"5511913412661@c.us"},"event":"messages"}'
Resposta esperada: {"success":true, ...} ou {"skipped":true,"reason":"no_tasks"}
`
Verificar tarefas "todo" de um responsável:
`bash
curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/tasks?responsible_id=eq.ID&status=eq.todo&deleted_at=is.null&select=id,title,status" \
-H "apikey: ANON_KEY"
`