Falhas no Envio de Mensagens (Situações e Causas)
Documento técnico: quais são as situações em que o envio de uma mensagem pode falhar na Pilot Status, considerando validações de API, enfileiramento, processamento no worker e feedback assíncrono via webhooks.
Este documento complementa a análise detalhada do fluxo em analise-fluxo-envio-mensagens.md.
1) Antes de enfileirar (falhas síncronas na API)
Estas falhas acontecem durante a chamada HTTP (a mensagem não chega a ser enfileirada no BullMQ).
1.1 Autenticação e autorização
- Sem
x-api-key(API pública) →401 x-api-keyinválida →401- Projeto em LIVE sem “aprovação de produção” →
403(PROJECT_NOT_APPROVED) - Rotas internas (dashboard/onboarding) sem sessão →
401
1.2 Validação de payload e parâmetros
- Body inválido no
sendMessageSchema→400 - Nem
destinationNumbernemgroupId→400 deliverAt/deliverUntilinválidos →400- Rotas internas exigem
destinationNumbere/oubodyOverride(draft) →400
1.3 Regras de TEST (destino permitido)
- Nenhum usuário do tenant tem telefone no perfil →
403- API pública: v1/messages/send/route.ts
- Rotas internas: internal/templates/[id]/test/route.ts, internal/templates/test-draft/route.ts
- Em TEST, envio para número que não seja o(s) do perfil (exceto grupo) →
403
1.4 Template (não encontrado / não aprovado / incompatível)
- Não encontra “versão aprovada do template” no contexto do request →
404(com códigos detalhando a causa) - Rotas de teste de template:
- Template inexistente →
404(internal/templates/[id]/test/route.ts) - Template sem versões (fluxo “normal”) →
404(internal/templates/[id]/test/route.ts)
- Template inexistente →
1.5 Instância WhatsApp (origem) e configuração
- Instância vinculada na API key mas inexistente →
409 - Instância existe mas não está conectada (
state !== OPEN) →409 - Sem instância no request e sem
EVOLUTION_INSTANCE_NAMEconfigurado →500
1.6 Restrições por categoria de template
- Template
MARKETINGusando número “default” da Pilot Status →403(TEMPLATE_CATEGORY_MARKETING_REQUIRES_OWN_NUMBER)- API pública: v1/messages/send/route.ts
- Rotas internas: internal/templates/[id]/test/route.ts
1.7 Opt-in (transacional)
- Destino não autorizado (não fez opt-in para transacionais) →
403- API pública: v1/messages/send/route.ts e catch v1/messages/send/route.ts
- Rotas internas: internal/onboarding/test-message/route.ts e catch internal/onboarding/test-message/route.ts
Observações:
- Para envio para grupo (
groupId) o opt-in é ignorado.
1.8 Rate limit / assinatura / pacotes
- Rate limit excedido →
429 - Causas típicas dentro do rate limit:
- Sem assinatura ativa (bloqueia envio).
- Estouro do limite diário (TEST) ou mensal/diário (LIVE/FREE).
- Auto-recharge pode tentar comprar pacote quando habilitado; falhas no auto-recharge não liberam limite automaticamente.
- rate-limit.service.ts
1.9 Falhas internas (exceções)
- Erros não mapeados (ex.: DB indisponível, bug, exceções de serviços internos) →
500- Exemplo (API pública): catch genérico v1/messages/send/route.ts
2) Após enfileirar (falhas assíncronas no worker)
Nestes casos a API normalmente responde sucesso (a mensagem foi criada com status=QUEUED), mas o envio pode falhar depois.
2.1 Lock / idempotência / duplicidade
- Worker não adquire lock Redis (
lock:ps:message:<id>) → job é ignorado sem erro (a mensagem permanece como está; tipicamenteQUEUED) - Mensagem não existe no banco → job “morre” sem envio (log e retorno)
- Mensagem não está
QUEUED→ worker não envia (idempotência por estado)
2.2 Expiração (deliverUntil)
- Quando
now > deliverUntil→ marcaFAILEDe emitemessage.failed(se houver webhook do cliente)
2.3 Dados insuficientes ou inconsistentes para enviar
Situações comuns:
- Template/body ausente ou inconsistência em modo
bodyOverride. - Instância WhatsApp não determinada (nem na mensagem/job, nem via
EVOLUTION_INSTANCE_NAME). - Campos mínimos necessários ausentes no registro.
Trechos:
- Validações/renderização que podem resultar em
FAILED: send-message.ts - Mensagens “órfãs”/inválidas também podem ser marcadas como
FAILEDpelo reconciliador: message-reconciler.ts
2.4 Falhas ao chamar a Evolution API (provider WhatsApp)
Mesmo com template válido, o envio pode falhar na integração com a Evolution:
- Instância removida (erro “NotFound”) → marca
FAILED, tenta marcar instância comoCLOSEe disparamessage.failed(sem retry do BullMQ) - Instância desconectada / conectando:
- Com tentativas restantes → lança erro para o BullMQ retentar (backoff fixo 30s, até 10 tentativas)
- Sem tentativas restantes → mantém
QUEUEDpara o reconciliador tentar mais tarde - send-message.ts
- Erro genérico na última tentativa → marca
FAILED, disparamessage.failede finaliza como job falho (sem novas tentativas)
Configuração da integração (base URL/API key) está centralizada em whatsapp.ts.
2.5 Reconciliação (mensagem fica QUEUED tempo demais)
O reconciliador é uma “rede de segurança” para mensagens que ficaram presas em QUEUED por inconsistência entre DB e fila:
- Mensagem expirada →
FAILEDe remoção de job (se existir) - Mensagem
QUEUEDhá tempo demais (timeout padrão 7 dias) →FAILED - Job sumiu da fila → reenfileira
- Job está
failed/completedmas DB aindaQUEUED→ reenfileira ou corrige
Referência: message-reconciler.ts
3) “Falha de envio” vs “falha de visibilidade de status” (webhooks)
Existe um caso comum onde o envio pode até ter ocorrido, mas o sistema não consegue confirmar/atualizar o status corretamente.
3.1 Status SENT/DELIVERED/READ depende de webhook assíncrono
Após a chamada bem-sucedida para a Evolution, o worker grava evolutionKeyId e evolutionInstanceId, mas o status SENT “oficial” depende do webhook messages.update (ack assíncrono).
- Persistência pós-chamada (sem marcar SENT): send-message.ts
- Handler de atualização de status (ack → SENT/DELIVERED/READ; erro → FAILED): messages-update.ts
Consequências:
- Se o webhook não chegar, a mensagem pode ficar como
QUEUEDno banco mesmo tendo sido enviada. - O reconciliador pode reenfileirar mais tarde, aumentando risco de duplicata.
3.2 Falhas na ingestão do webhook da Evolution (RabbitMQ → webhook interno)
Quando a ingestão está via RabbitMQ consumer, falhas ao encaminhar o evento da Evolution para o webhook interno disparam retries em filas .retry.* e, após exceder, enviam para .dlq.
- Lógica de retry/DLQ: evolution-consumer.ts
Isso pode causar “atraso” ou “perda” de acks de status (ex.: SERVER_ACK), afetando a visibilidade do status no DB e os webhooks do cliente.
4) Como a falha aparece para o cliente
4.1 Na resposta HTTP (falhas síncronas)
- O request falha com
4xx/5xxe o cliente recebe o motivo (às vezes com umcodeespecífico, dependendo da rota). - A mensagem normalmente não é criada nem enfileirada quando a falha é de validação/auth/rate-limit antes do
MessageService.send().
4.2 Como status de mensagem (falhas assíncronas)
message.statuspode ir paraFAILEDcomerrorMessage(ex.: expirou, dados inválidos, erro na Evolution).- A transição para
SENT/DELIVERED/READé baseada no webhookmessages.update.
4.3 Via webhook outbound do cliente (message.failed)
- Quando o sistema marca uma mensagem como
FAILED, ele tenta notificar o cliente via webhook configurado. - Em caso de indisponibilidade do endpoint do cliente, o envio do webhook é “best-effort” (sem retry dedicado).
5) Checklist rápido de diagnóstico
- O envio falhou na hora (API retornou 4xx/5xx)?
- A mensagem existe no banco e está
QUEUEDhá muito tempo? deliverUntiljá expirou (especialmente OTP)?- A instância está
OPENno banco e disponível na Evolution? - Há
evolutionKeyIdpreenchido, mas status aindaQUEUED(suspeita de falha/atraso no webhookmessages.update)? - Em ingestão via RabbitMQ, há eventos acumulando em
.retry.*/.dlq?