Pular para o conteúdo principal

Guia anti-ban

Este é o guia mais importante desta documentação. Se você só vai ler uma página antes de disparar mensagens em produção, leia esta.

1. Por que este guia existe

Esta API automatiza o WhatsApp Web através de whatsapp-web.js — uma biblioteca não oficial que controla uma sessão de navegador, e não a API Business oficial da Meta. A Meta tem meios de detectar automação não oficial (padrões de envio, ausência de respostas, denúncias de destinatários, comportamento fora do que um humano faria), e o desfecho típico é o banimento do número.

O AntiBanGuard (src/core/AntiBanGuard.ts) existe para reduzir esse risco: impõe opt-in, respeita uma janela de horário, limita o volume diário, espaça os envios, ritma o crescimento de um número novo (warmup) e pausa os envios proativos quando os sinais de saúde da instância pioram. Nada disso é garantia — veja o callout de honestidade na seção 10.

2. Os gates, na ordem real

Todo envio proativo (mensagem que a instância inicia, sem alguém ter escrito primeiro) passa por evaluateProactive (src/core/AntiBanGuard.ts) antes de sair. Os gates rodam nesta ordem exata, e a primeira recusa interrompe a cadeia:

#GateO que verificaSe falha
1Opt-inisOptedIn(phone) — o número tem opt-in registrado nesta instância? (só roda se requireOptIn estiver ligado e force não tiver sido passado)Rejeitacode: optin_required, reason: "Numero sem opt-in registrado"
2BlacklistisBlacklisted(phone) — o número está na blacklist da instância?Rejeitacode: blacklisted, reason: "Numero em blacklist da instancia"
3Existência no WhatsAppnumberExists(phone) — o número existe de fato no WhatsApp?Rejeitacode: number_not_found, reason: "Numero nao existe no WhatsApp"
4Circuit breaker (vermelho)healthState === 'red'?Rejeitacode: circuit_open, reason: "Instancia em estado vermelho: envios proativos pausados"
5Janela de horárioO horário atual (no timezone da instância) está entre windowStart e windowEnd?Não rejeitaagenda para a próxima abertura da janela (nextWindowOpen)
6Limite diário (warmup + amarelo + curva intradiária)Quantas proativas já saíram hoje, contra a cota do dia?Não rejeitaagenda para mais tarde hoje (curva intradiária) ou para a próxima janela (cota do dia esgotada)

Os quatro primeiros gates são binários: ou o número passa, ou o envio é recusado com um reason específico (útil para logar e depurar por que um envio não saiu). Os dois últimos nunca recusam — eles adiam o envio para um horário melhor, devolvendo { allow: true, scheduleFor: <data> } em vez de uma recusa.

O gate 6 combina três regras, aplicadas em sequência:

  1. A cota do dia (dayQuota) vem de warmupLimitForDay (seção 5).
  2. Se o estado de saúde for amarelo, a cota do dia é cortada pela metade (Math.floor(dayQuota / 2)).
  3. Se a cota do dia inteira já foi usada, agenda para a próxima janela (amanhã). Senão, se intradayCurveEnabled estiver ligado (padrão), ritma o restante da cota ao longo da janela do dia — sem nunca empurrar um envio que ainda cabe na cota diária para o dia seguinte.

3. force: o que ele realmente dispensa

A flag force (parâmetro de evaluateProactive) dispensa apenas o gate 1 (opt-in). Olhando o código: if (cfg.requireOptIn && !opts.force) é a única condição que lê opts.force em toda a função. Os gates 2, 3, 4, 5 e 6 rodam exatamente igual, force ou não.

Ou seja: force nunca dispensa a blacklist, nunca dispensa o circuit breaker vermelho, e nunca salta a janela de horário ou o limite diário. É uma válvula pontual para o caso legítimo de enviar a um número sem opt-in formal registrado no sistema (ex.: você tem consentimento por outro canal) — não é um botão de "ignorar o antiban".

4. Reativo vs. proativo

O AntiBanGuard só se aplica a envios proativos (proactive: true) — mensagens que a instância inicia por conta própria (campanhas, cobranças, lembretes). Um envio reativo (proactive: false, o padrão), ou seja, uma resposta a alguém que escreveu primeiro, não passa por nenhum dos seis gates e vai direto para a fila, sem espaçamento artificial.

Isso é proposital: responder quem já iniciou contato é comportamento saudável de qualquer conta de WhatsApp, humana ou não, e não é o tipo de padrão que a Meta associa a automação abusiva. Represar respostas atrás dos mesmos limites de volume e espaçamento de uma campanha proativa não reduziria risco — só pioraria a experiência de quem já escreveu para você.

5. Warmup

warmupLimitForDay (src/core/warmup.ts) decide a cota diária de um número novo ou recém-conectado, com base em há quantos dias o warmup começou (warmupStartDate, guardado em Instance e reiniciável pelo operador ao trocar o chip):

SituaçãoCota do dia
warmupStartDate não definidodailyLimit (cota cheia, padrão 200)
Menos de 7 dias desde o início do warmup20 (week1)
De 7 a 13 dias desde o início do warmup40 (week2)
14 dias ou mais desde o início do warmupdailyLimit (cota cheia, padrão 200)

week1 e week2 são constantes fixas no serviço (src/services/AntiBanService.ts: DEFAULT_WEEK1 = 20, DEFAULT_WEEK2 = 40) — não são configuráveis por instância hoje. dailyLimit é a única parte configurável dessa cota (AntiBanConfig.dailyLimit, padrão 200), e o warmup só se aplica se warmupEnabled estiver ligado (padrão: ligado).

Um número que acabou de ser pareado deveria começar o warmup imediatamente — um número aquecendo por dias sem warmupStartDate definido usa a cota cheia desde o primeiro dia, o que anula o propósito do warmup.

6. Espaçamento entre envios

Cada envio proativo aguarda um intervalo aleatório entre sendDelayMinSec e sendDelayMaxSec em relação ao anterior da mesma instância — os padrões em AntiBanConfig são 30 a 90 segundos (sendDelayMinSec=30, sendDelayMaxSec=90). Quando o estado de saúde está amarelo, os dois limites dobram (src/services/MessageService.ts e src/core/queues/sendQueue.ts: if (guardCfg.healthState === 'yellow') { minSec *= 2; maxSec *= 2; }) — uma instância já em alerta espaça o envio proativo, além de enviar menos por dia.

Esse espaçamento é calculado no enqueue (MessageService.send), não como uma pausa dentro do worker — o delay vira o delay do próprio job na fila (BullMQ), para não travar mensagens reativas atrás de uma pausa longa.

Imediatamente antes de cada envio proativo sair de fato, o worker (processSendJob, src/core/queues/sendQueue.ts) reavalia os seis gates do zero, com os dados já atualizados no momento do envio (contador diário já reflete os envios anteriores da mesma rajada, o estado de saúde pode ter mudado desde o enfileiramento). Isso existe porque o gate feito no enqueue lê o contador do dia antes de qualquer envio da rajada ter acontecido — sem essa reavaliação, uma rajada de proativas enfileiradas ao mesmo tempo leria todas o mesmo contador desatualizado e todas passariam pelo limite diário. A reavaliação no worker fecha essa janela, e também re-gateia envios que ficaram agendados de um dia para o outro (ex.: uma instância que ficou vermelha durante a noite não dispara mais o envio agendado).

7. Estados de saúde

computeHealth (src/core/health.ts) classifica cada instância em verde, amarelo ou vermelho, com base em métricas recalculadas a cada 5 minutos pelo scheduler (src/schedulers/index.ts). Os limiares reais:

🔴 Vermelho se qualquer uma destas condições for verdadeira:

  • reports >= 3
  • blocks >= 10
  • recentDisconnects >= 5
  • sentProactiveWindow >= 20 e noReplyRate >= 0.95

🟡 Amarelo (só avaliado se nenhuma condição de vermelho bateu) se qualquer uma destas for verdadeira:

  • sent >= 20 e replied / sent < 0.15
  • blocks >= 3
  • sentProactiveWindow >= 20 e noReplyRate >= 0.85

🟢 Verde: nenhuma das condições acima.

noReplyRate = noReply48h / sentProactiveWindow (zero quando a janela está vazia). Não é coincidência que os limiares de noReplyRate (0.85 e 0.95) sejam os complementos do limiar de ratio (0.15): as duas regras julgam o mesmo sinal — taxa de resposta — por dois ângulos diferentes (contador absoluto de "sem resposta" vs. razão direta de resposta), e precisam se mover juntas para não se contradizerem.

As janelas usadas para calcular esses números não são todas iguais (MetricsService.recomputeHealth): blocks e reports olham só a janela de 1 dia (hoje, no timezone da instância) — são eventos pontuais que não precisam de histórico. Já sent/replied (a razão de resposta) usam uma janela de 7 dias, porque respostas chegam com latência e uma janela de 1 dia penalizaria injustamente instâncias saudáveis logo depois de uma rajada de envios matinal. noReply48h/sentProactiveWindow usam uma janela deslizante própria, entre 48 horas e 7 dias atrás.

Isso tem uma consequência prática: como blocks/reports resetam para a janela de hoje, o estado vermelho se recupera conforme os sinais envelhecem — um dia ruim não condena a instância para sempre, desde que novos blocks/reports não continuem chegando.

Quando o estado vira vermelho, dois efeitos entram em ação:

  • Gate 4 pausa todo envio proativo — só respostas reativas continuam saindo (seção 4).
  • O scheduler dispara o webhook instance.health (o "kill switch"; ver Webhooks) para a instância, se ela tiver webhookUrl configurada.

8. report-block: a única fonte de sinal de bloqueio/denúncia

A API não oficial do WhatsApp Web (whatsapp-web.js) não expõe quem bloqueou ou denunciou o número — não existe evento para isso na sessão. Sem uma fonte externa de sinal, os contadores blocks e reports ficam sempre em zero, e os gatilhos de vermelho reports >= 3 e blocks >= 10 nunca disparam, por mais problemática que a instância esteja na prática.

POST /instance/:name/report-block (src/routes/antiban.ts) é o único jeito de alimentar esse sinal: o operador (manualmente, pelo Portal de Controle, ou via API — a partir de retorno de cliente, CRM, ou qualquer outra fonte) registra um bloqueio ou denúncia com { phone, tipo: "block" | "report" }. O endpoint incrementa o contador diário do tipo informado e adiciona o número à blacklist da instância (quem bloqueou ou denunciou não deve receber mais nada — gate 2 da seção 2 cuida disso a partir daí).

Se sua operação não usa este endpoint, o circuit breaker de vermelho por blocks/reports está, na prática, desligado — os outros gatilhos (recentDisconnects >= 5, e a taxa de não-resposta) continuam funcionando independentemente, porque vêm de dados que a própria sessão já enxerga.

9. Boas práticas

  • Opt-in de verdade. O gate 1 verifica um registro (POST /optin), mas o valor dele depende de o consentimento por trás ser real — número comprado, capturado sem permissão explícita ou herdado de uma lista antiga é o cenário clássico que termina em denúncia.
  • Verifique o número antes de disparar. POST /chat/whatsappNumbers confere em lote quais números existem de fato no WhatsApp antes de uma campanha — evita gastar cota e piorar a reputação da instância enviando para números que o gate 3 vai rejeitar de qualquer forma (ou pior, que nem chegam a passar pelo gate porque a checagem não foi feita em lote antes).
  • Use Spintax para não repetir texto idêntico. renderSpintax (src/core/spintax.ts) resolve blocos {opção a|opção b|opção c} (aninháveis) e variáveis {{nome}} — mensagens em massa com o texto byte-a-byte idêntico são um padrão fácil de sinalizar como automação.
  • Respeite a janela de horário. windowStart/windowEnd (padrão 09:0018:00, timezone America/Cuiaba) existem para que os envios aconteçam num horário compatível com o de uma pessoa operando a conta — não desligue a curva intradiária nem force envios fora da janela.
  • Comece pequeno. Deixe o warmup ativo (padrão) em qualquer número novo ou recém-pareado, e não zere warmupStartDate só para "destravar" volume mais cedo — a cota reduzida das duas primeiras semanas (seção 5) é o próprio mecanismo de warmup.
Os limiares são o que o código faz — não uma garantia contra banimento

O AntiBanGuard nunca foi exercitado sob tráfego real e sustentado — os valores desta página (thresholds de saúde, janelas, warmup, espaçamento) são exatamente o que o código aplica hoje, verificados linha a linha contra src/core/health.ts, src/core/warmup.ts, src/core/AntiBanGuard.ts e prisma/schema.prisma. Isso não é o mesmo que uma garantia de que seguir estas regras evita o banimento: a Meta pode mudar critérios de detecção a qualquer momento, e o comportamento deste guard sob volume real de produção, por enquanto, não foi observado.