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:
| # | Gate | O que verifica | Se falha |
|---|---|---|---|
| 1 | Opt-in | isOptedIn(phone) — o número tem opt-in registrado nesta instância? (só roda se requireOptIn estiver ligado e force não tiver sido passado) | Rejeita — code: optin_required, reason: "Numero sem opt-in registrado" |
| 2 | Blacklist | isBlacklisted(phone) — o número está na blacklist da instância? | Rejeita — code: blacklisted, reason: "Numero em blacklist da instancia" |
| 3 | Existência no WhatsApp | numberExists(phone) — o número existe de fato no WhatsApp? | Rejeita — code: number_not_found, reason: "Numero nao existe no WhatsApp" |
| 4 | Circuit breaker (vermelho) | healthState === 'red'? | Rejeita — code: circuit_open, reason: "Instancia em estado vermelho: envios proativos pausados" |
| 5 | Janela de horário | O horário atual (no timezone da instância) está entre windowStart e windowEnd? | Não rejeita — agenda para a próxima abertura da janela (nextWindowOpen) |
| 6 | Limite diário (warmup + amarelo + curva intradiária) | Quantas proativas já saíram hoje, contra a cota do dia? | Não rejeita — agenda 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:
- A cota do dia (
dayQuota) vem dewarmupLimitForDay(seção 5). - Se o estado de saúde for amarelo, a cota do dia é cortada pela
metade (
Math.floor(dayQuota / 2)). - Se a cota do dia inteira já foi usada, agenda para a próxima janela
(amanhã). Senão, se
intradayCurveEnabledestiver 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ção | Cota do dia |
|---|---|
warmupStartDate não definido | dailyLimit (cota cheia, padrão 200) |
| Menos de 7 dias desde o início do warmup | 20 (week1) |
| De 7 a 13 dias desde o início do warmup | 40 (week2) |
| 14 dias ou mais desde o início do warmup | dailyLimit (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 >= 3blocks >= 10recentDisconnects >= 5sentProactiveWindow >= 20enoReplyRate >= 0.95
🟡 Amarelo (só avaliado se nenhuma condição de vermelho bateu) se qualquer uma destas for verdadeira:
sent >= 20ereplied / sent < 0.15blocks >= 3sentProactiveWindow >= 20enoReplyRate >= 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 tiverwebhookUrlconfigurada.
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/whatsappNumbersconfere 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ão09:00–18:00, timezoneAmerica/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
warmupStartDatesó para "destravar" volume mais cedo — a cota reduzida das duas primeiras semanas (seção 5) é o próprio mecanismo de warmup.
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.