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 | getPublicApiKeyInfo → ApiKeyService.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 202 → worker 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:
-
Ambiente LIVE e projeto
Seenvironment === LIVE, o projeto precisa estar aprovado para produção (productionApproved). Caso contrário:403com códigoPROJECT_NOT_APPROVED. -
Corpo da requisição
Validação comsendMessageSchema(@pilot-status/shared). Erros:400comdetailsdo Zod. -
Destino
ExigedestinationNumberougroupId. 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). -
Datas opcionais
deliverAt/deliverUntilnormalizados e validados;deliverAtinválido →400. -
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
404incluem códigos comoTEMPLATE_NOT_FOUND,TEMPLATE_PROJECT_MISMATCH,TEMPLATE_ENV_MISMATCH,TEMPLATE_NOT_APPROVED,TEMPLATE_NOT_APPROVED_FOR_PRODUCTION, conforme o caso.
- Busca a versão mais recente aprovada via
-
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). -
Instância WhatsApp
- Se a API key tem
whatsappInstanceId: a instância precisa existir no tenant (whatsapp_instances). O estadostateno banco não bloqueia o enqueue (ex.:CLOSEainda recebe202); conexão real é tratada no worker ao chamar a Evolution. - Se a instância vinculada não existir (API key apontando para id removido):
409comcode: LINKED_WHATSAPP_INSTANCE_NOT_FOUND, mensagem amigável, e é criado um registromessagecomstatus: FAILED(aparece em/logscomerrorCodederivado). - 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.
- Se a API key tem
-
Verificação de número no WhatsApp (não grupo)
ChamacheckWhatsappExistscom oinstanceNameescolhido. Se o número não existir na rede: cria mensagemFAILEDe retorna404comDESTINATION_NOT_FOUND. -
Marketing no número Pilot Status
Template categoriaMARKETINGnão pode usar a instância default do Pilot Status:403/TEMPLATE_CATEGORY_MARKETING_REQUIRES_OWN_NUMBER. -
Opt-in transacional
WhatsAppTransactionalOptInService.assertDestinationAuthorized— exceto quando bypass: destino é grupo, ou é número Pilot Status com template categoriaOTP. Falha:403(DestinationNotAuthorizedError). -
Rate limit / plano
RateLimitService.check. Falha:429. -
Labels (opcional)
Se o payload incluirlabels, enfileira job assíncronolabels-upsert(best-effort; falha ao enfileirar só loga erro). -
Envio
MessageService.send(...)comskipOptInValidation: true(opt-in já validado no handler). -
Resposta de sucesso
202 Acceptedcomid,correlationId,status(tipicamenteQUEUED),createdAt,origin(nome de exibição ouinstanceNameda instância).
O que MessageService.send faz
Arquivo: apps/fullstack/src/services/message.service.ts.
- Resolve ou cria o projeto (default slug
defaultse não houverprojectIdna 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
deliverUntilpadrão se o cliente não enviar: OTP 2 min, UTILITY 1 h, MARKETING 4 h. - Cria registro
messagecomstatus: QUEUED,deliverBy/deliverUntil,correlationId, vínculos com tenant, projeto, versão de template, instância e API key. - Enfileira job BullMQ nome
send-messagena fila da instância (getQueue), comjobId = message.id, até 10 tentativas, backoff fixo 30s,delaysedeliverByfor futuro, prioridade conformeurgent.
Depois do 202: worker e webhooks
- Worker:
apps/worker/src/send-message.ts(processMessage) — lock Redis, validaQUEUEDe janeladeliverUntil, 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, atualizawhatsapp_instances.stateparaOPEN(elastValidatedAt) e reenvia imediatamente uma vez, sem esperar o próximo backoff do BullMQ (mantendoattempts/reconciler como hoje). - Webhook Evolution:
apps/fullstack/src/app/api/internal/webhook/— eventos comomessages.updatepromovemSENT/DELIVERED/READ/FAILEDno 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/shared — sendMessageSchema |
| 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) |