Documentação / Fluxo: envio de mensagem pela API pública

Fluxo: envio de mensagem pela API pública

Entrar

Fluxo: envio de mensagem pela API pública

Este documento descreve o caminho percorrido quando um cliente chama o endpoint público de envio de mensagens com template. Para análise aprofundada (fila, worker, reconciler, riscos de duplicidade e webhooks), consulte também message-analisys/fluxo-envio-mensagens.md.

Endpoint e autenticação

| Item | Valor | |------|--------| | Método e rota | POST /api/v1/messages/send | | Implementação | apps/fullstack/src/app/api/v1/messages/send/route.ts | | Autenticação | Header x-api-key (obrigatório) com o segredo da API key; opcionalmente x-api-key-id para consistência cruzada | | Validação da chave | getPublicApiKeyInfoApiKeyService.validate / validateById (apps/fullstack/src/lib/public-api-auth.ts) |

A função POST apenas autentica, faz request.json() e delega para handleSendWithKeyInfo({ keyInfo, body }), onde está toda a regra de negócio.

Visão em uma linha

Cliente → validações (projeto, payload, template, instância WhatsApp, destino, opt-in, rate limit) → MessageService.send (grava message + enfileira job BullMQ) → resposta 202worker consome a fila, chama a Evolution, e o webhook interno atualiza status da mensagem.

flowchart LR
  subgraph api [Fullstack API]
    A[POST /api/v1/messages/send]
    V[Validações]
    M[MessageService.send]
    A --> V --> M
  end
  subgraph async [Assíncrono]
    Q[(BullMQ / Redis)]
    W[Worker send-message]
    E[Evolution API]
    WH[Webhook interno]
  end
  DB[(Postgres)]
  M --> DB
  M --> Q
  Q --> W
  W --> E
  E -.-> WH
  WH --> DB

Etapas do handler público (handleSendWithKeyInfo)

Ordem lógica conforme o código em route.ts:

  1. Ambiente LIVE e projeto
    Se environment === LIVE, o projeto precisa estar aprovado para produção (productionApproved). Caso contrário: 403 com código PROJECT_NOT_APPROVED.

  2. Corpo da requisição
    Validação com sendMessageSchema (@pilot-status/shared). Erros: 400 com details do Zod.

  3. Destino
    Exige destinationNumber ou groupId. Em TEST, grupos não são permitidos (403). Em TEST, o destino deve ser um dos números permitidos (perfis do tenant + número Pilot Status, se configurado).

  4. Datas opcionais
    deliverAt / deliverUntil normalizados e validados; deliverAt inválido → 400.

  5. Resolução do template

    • Busca a versão mais recente aprovada via TemplateService.getLatestVersionForTenant (aceita id ou nome).
    • Em LIVE, a versão efetiva precisa ter approvalStatus === APPROVED.
    • Respostas 404 incluem códigos como TEMPLATE_NOT_FOUND, TEMPLATE_PROJECT_MISMATCH, TEMPLATE_ENV_MISMATCH, TEMPLATE_NOT_APPROVED, TEMPLATE_NOT_APPROVED_FOR_PRODUCTION, conforme o caso.
  6. Assinatura e mídia (LIVE)
    Se o template em LIVE tiver mídia no corpo, pode exigir assinatura ativa paga (402 / SUBSCRIPTION_REQUIRED_FOR_MEDIA).

  7. Instância WhatsApp

    • Se a API key tem whatsappInstanceId: a instância precisa existir no tenant (whatsapp_instances). O estado state no banco não bloqueia o enqueue (ex.: CLOSE ainda recebe 202); conexão real é tratada no worker ao chamar a Evolution.
    • Se a instância vinculada não existir (API key apontando para id removido): 409 com code: LINKED_WHATSAPP_INSTANCE_NOT_FOUND, mensagem amigável, e é criado um registro message com status: FAILED (aparece em /logs com errorCode derivado).
    • Se não há instância na key: usa EVOLUTION_INSTANCE_NAME (instância default, ex. número Pilot Status). Se faltar configuração: 500.
  8. Verificação de número no WhatsApp (não grupo)
    Chama checkWhatsappExists com o instanceName escolhido. Se o número não existir na rede: cria mensagem FAILED e retorna 404 com DESTINATION_NOT_FOUND.

  9. Marketing no número Pilot Status
    Template categoria MARKETING não pode usar a instância default do Pilot Status: 403 / TEMPLATE_CATEGORY_MARKETING_REQUIRES_OWN_NUMBER.

  10. Opt-in transacional
    WhatsAppTransactionalOptInService.assertDestinationAuthorized — exceto quando bypass: destino é grupo, ou é número Pilot Status com template categoria OTP. Falha: 403 (DestinationNotAuthorizedError).

  11. Rate limit / plano
    RateLimitService.check. Falha: 429.

  12. Labels (opcional)
    Se o payload incluir labels, enfileira job assíncrono labels-upsert (best-effort; falha ao enfileirar só loga erro).

  13. Envio
    MessageService.send(...) com skipOptInValidation: true (opt-in já validado no handler).

  14. Resposta de sucesso
    202 Accepted com id, correlationId, status (tipicamente QUEUED), createdAt, origin (nome de exibição ou instanceName da instância).

O que MessageService.send faz

Arquivo: apps/fullstack/src/services/message.service.ts.

  • Resolve ou cria o projeto (default slug default se não houver projectId na key).
  • Opcionalmente revalida opt-in (no fluxo público de send isso vem desligado via skipOptInValidation).
  • Aplica retenção da API key (retentionDays): pode redigir destino/payload no banco.
  • Lê a categoria do template para definir deliverUntil padrão se o cliente não enviar: OTP 2 min, UTILITY 1 h, MARKETING 4 h.
  • Cria registro message com status: QUEUED, deliverBy / deliverUntil, correlationId, vínculos com tenant, projeto, versão de template, instância e API key.
  • Enfileira job BullMQ nome send-message na fila da instância (getQueue), com jobId = message.id, até 10 tentativas, backoff fixo 30s, delay se deliverBy for futuro, prioridade conforme urgent.

Depois do 202: worker e webhooks

  • Worker: apps/worker/src/send-message.ts (processMessage) — lock Redis, valida QUEUED e janela deliverUntil, renderiza template, opcionalmente presença “digitando”, chama Evolution (sendText / sendButtons), trata retries e falhas. Em erro típico de desconexão, faz uma checagem ao vivo (connectionState + setPresence); se confirmar sessão aberta, atualiza whatsapp_instances.state para OPEN (e lastValidatedAt) e reenvia imediatamente uma vez, sem esperar o próximo backoff do BullMQ (mantendo attempts/reconciler como hoje).
  • Webhook Evolution: apps/fullstack/src/app/api/internal/webhook/ — eventos como messages.update promovem SENT / DELIVERED / READ / FAILED no registro da mensagem.

Detalhes de estados, reconciler e pontos de falha: fluxo-envio-mensagens.md.

Respostas HTTP mais comuns (resumo)

| Status | Situação típica | |--------|------------------| | 202 | Mensagem aceita, gravada como QUEUED, job criado | | 400 | Payload inválido (sendMessageSchema) | | 401 | API key ausente ou inválida | | 403 | LIVE sem aprovação; TEST destino/grupo não permitido; opt-in negado; marketing na instância default | | 404 | Template/versão não encontrada ou não aprovada; destino não existe no WhatsApp | | 409 | Instância vinculada inexistente (code: LINKED_WHATSAPP_INSTANCE_NOT_FOUND; gera message FAILED para logs) | | 429 | Rate limit / cota | | 402 | LIVE com mídia no template sem assinatura paga (quando aplicável) | | 500 | Erro interno ou instância default não configurada |

Arquivos de referência rápida

| Peça | Caminho | |------|---------| | Rota HTTP pública | apps/fullstack/src/app/api/v1/messages/send/route.ts | | Auth de API pública | apps/fullstack/src/lib/public-api-auth.ts | | Persistência + fila | apps/fullstack/src/services/message.service.ts | | Schema do body | packages/sharedsendMessageSchema | | Worker | apps/worker/src/send-message.ts | | Fila | apps/fullstack/src/lib/queue.ts + packages/shared/src/constants.ts (nome da fila por instância) |