Apostila Completa
🎯

CaptaCliente

SaaS de Prospecção de Leads
Da arquitetura ao deploy — caso real completo

API de Monitoramento
WhatsApp + Evolution API
React
Node.js
Express
MySQL
TypeScript
Evolution API
techfluxar.com | Março 2026

Sumário

Sobre esta apostila: Este material cobre o CaptaCliente como caso prático real. Inclui a API de monitoramento (backend) e a integração com WhatsApp via Evolution API.
PARTE 1 — O Sistema CaptaCliente
M1   O que é o CaptaCliente
Arquitetura, banco de dados, tabelas principais
M2   O Desafio: API de Monitoramento
Dados, pensamento de programador
M3   Passo a Passo: Construindo a API
Rota, controller, app.js, testar
M4   SQL Usado no CaptaCliente
COUNT, WHERE, GROUP BY, SUM
M5   Conceitos JavaScript Usados
Desestruturação, async/await, try/catch
M6   Erros Reais e Como Resolver
Conexão recusada, erro 500, typos SQL
M7   Resultado Final
JSON da API, fluxo completo
PARTE 2 — WhatsApp com Evolution API
W8.1   Arquitetura
Como o CaptaCliente usa a Evolution API
W8.2   Instalando a Evolution API
Docker, container, verificação
W8.3   O Service (evolutionService.js)
Encapsulamento da API, métodos
W8.4   O Controller (rotas REST)
connect, send, webhook, status
W8.5   Webhook (receber respostas)
Evolution envia POST, salvar no banco
W8.6   Frontend (página WhatsApp)
React, QR Code, status
W8.7   Configuração (.env)
Variáveis de ambiente
W8.8   Fluxo Completo
Do clique ao lead respondendo
W8.9   Endpoints da Evolution API
Referência rápida completa
Bônus - Caso Prático Real
CaptaCliente

Tudo que você construiu na prática:
uma API de monitoramento do zero.

M1 O que é o CaptaCliente

O CaptaCliente é um SaaS (Software as a Service) de prospecção de leads. Ele faz:

Arquitetura do sistema

┌──────────────────────────────────────────────────┐ │ CaptaCliente │ ├──────────────────────────────────────────────────┤ │ │ │ Frontend (React + TypeScript + Vite) │ │ └── captacliente.techfluxar.com │ │ └── Servido pelo Nginx (pasta dist/) │ │ │ │ Backend (Node.js + Express + MySQL) │ │ └── Roda na porta 3737 │ │ └── Nginx faz proxy: /api/ → localhost:3737 │ │ │ │ Scraper (Google Maps) │ │ └── Busca leads automaticamente │ │ │ │ Worker (Envio de mensagens) │ │ └── Processa filas de envio │ │ │ │ PM2 (Gerenciador de processos) │ │ └── Mantém tudo rodando 24/7 │ │ │ └──────────────────────────────────────────────────┘

Banco de dados - Tabelas principais

Tabela O que guarda
users Usuários do sistema (nome, email, senha, plano)
leads Leads capturados (nome, telefone, endereço, status)
campanhas Campanhas de mensagens (nome, status, mensagem)
faturas Cobranças e pagamentos (valor, status, vencimento)
sugestoes Sugestões de busca automática
M2 O Desafio: API de Monitoramento

O objetivo: criar uma API que retorne os dados principais do CaptaCliente para um app de monitoramento em React Native.

Dados que a API precisa retornar: 1. Total de leads cadastrados
2. Leads separados por status (novo, contatado, respondeu)
3. Quantidade de usuários ativos
4. Campanhas ativas e inativas
5. Faturamento mensal

Pensamento de programador aplicado

Pergunta Resposta
O que preciso entregar? Uma URL que devolve dados do sistema em JSON
Que dados? Leads, usuários, campanhas, faturamento
De onde vêm? Banco de dados MySQL (tabelas: leads, users, campanhas, faturas)
Como entrego? Rota GET que retorna JSON
O que preciso criar? 1 rota + 1 controller + conectar no app.js
M3 Passo a Passo: Construindo a API

Passo 1: Criar a rota (monitorRoutes.js)

A rota é o "endereço" da API. Ela define: quando alguém acessar GET /monitor/stats, execute a função getStats.

// backend/src/routes/monitorRoutes.js

// 1. Importa o Express (biblioteca que cria servidores web)
const express = require('express');

// 2. Cria um "mini-servidor" de rotas
const router = express.Router();

// 3. Importa o controller (quem vai fazer o trabalho)
const monitorController = require('../controllers/monitorController');

// 4. Define: GET /stats → executa getStats do controller
router.get('/stats', monitorController.getStats);

// 5. Exporta para ser usado em outro arquivo
module.exports = router;
Entendendo router.get('/stats', monitorController.getStats) router.get = quando alguém fizer uma requisição GET
'/stats' = nesse endereço
monitorController.getStats = execute essa função

Passo 2: Criar o controller (monitorController.js)

O controller é quem faz o trabalho pesado: consulta o banco de dados e monta a resposta.

// backend/src/controllers/monitorController.js

// Importa o pool de conexões do banco
// ATENÇÃO: usa { pool } com chaves! Isso é desestruturação.
const { pool } = require('../database/connection');

exports.getStats = async (req, res) => {
  try {
    // Conta total de leads
    const [leadsCount] = await pool.query(
      'SELECT COUNT(*) AS totalLeads FROM leads'
    );

    // Conta leads agrupados por status
    const [leadsByStatus] = await pool.query(
      'SELECT status, COUNT(*) AS count FROM leads GROUP BY status'
    );

    // Conta usuários ativos
    const [userCount] = await pool.query(
      'SELECT COUNT(*) AS totalUsers FROM users WHERE ativo = 1'
    );

    // Conta campanhas ativas
    const [campanhasAtivas] = await pool.query(
      'SELECT COUNT(*) AS totalCampanhasAtivas FROM campanhas WHERE status = "ativa"'
    );

    // Conta campanhas inativas
    const [campanhasInativas] = await pool.query(
      'SELECT COUNT(*) AS totalCampanhasInativas FROM campanhas WHERE status = "inativa"'
    );

    // Soma faturamento pago
    const [faturamentoPago] = await pool.query(
      'SELECT SUM(valor) AS faturamentoMensal FROM faturas WHERE status = "pago"'
    );

    // Monta e retorna o JSON de resposta
    res.json({
      totalLeads: leadsCount[0].totalLeads,
      leadsByStatus: leadsByStatus.map(row => ({
        status: row.status,
        count: row.count
      })),
      totalUsers: userCount[0].totalUsers,
      totalCampanhasAtivas: campanhasAtivas[0].totalCampanhasAtivas,
      totalCampanhasInativas: campanhasInativas[0].totalCampanhasInativas,
      faturamentoMensal: faturamentoPago[0].faturamentoMensal || 0
    });
  } catch (error) {
    console.error('Erro ao obter estatísticas:', error);
    res.status(500).json({ error: 'Erro ao obter estatísticas' });
  }
};

Passo 3: Conectar no app.js

O app.js é o "ponto central" do backend. Toda rota nova precisa ser registrada aqui.

// No arquivo backend/src/app.js, adicionar 2 linhas:

// 1. Importar a rota (no topo do arquivo, junto com as outras)
const monitorRoutes = require('./routes/monitorRoutes');

// 2. Conectar a rota (junto com os outros app.use)
app.use('/monitor', monitorRoutes);

// Resultado: GET /monitor/stats → monitorController.getStats
Por que /monitor/stats e não /stats? O app.use('/monitor', ...) define o prefixo /monitor.
Dentro da rota, router.get('/stats', ...) define o complemento /stats.
O Express junta: /monitor + /stats = /monitor/stats

Passo 4: Reiniciar e testar

$ pm2 restart captacliente-api
# Reinicia o servidor para carregar as mudanças

$ curl http://localhost:3737/monitor/stats
# Testa a API no terminal
M4 SQL Usado no CaptaCliente

Cada query usada no controller, explicada em detalhe:

COUNT(*) - Contar registros

-- Conta TODOS os leads
SELECT COUNT(*) AS totalLeads FROM leads;
-- COUNT(*) = conta todas as linhas da tabela
-- AS totalLeads = dá um nome ao resultado
-- Resultado: { totalLeads: 212 }

COUNT + WHERE - Contar com filtro

-- Conta só usuários ATIVOS
SELECT COUNT(*) AS totalUsers FROM users WHERE ativo = 1;
-- WHERE ativo = 1 = só conta quem tem ativo = 1 (verdadeiro)

-- Conta campanhas com status "ativa"
SELECT COUNT(*) AS total FROM campanhas WHERE status = "ativa";
-- WHERE status = "ativa" = filtra só as ativas

GROUP BY - Agrupar resultados

-- Conta leads SEPARADOS por status
SELECT status, COUNT(*) AS count FROM leads GROUP BY status;
-- GROUP BY status = cria um grupo pra cada status diferente
-- Resultado:
-- | status    | count |
-- | novo      | 90    |
-- | contatado | 77    |
-- | respondeu | 45    |

SUM - Somar valores

-- Soma o valor de todas as faturas pagas
SELECT SUM(valor) AS faturamentoMensal FROM faturas WHERE status = "pago";
-- SUM(valor) = soma todos os valores da coluna "valor"
-- Se não tiver nenhuma fatura paga, retorna NULL
-- Por isso usamos || 0 no JavaScript (se for null, retorna 0)
M5 Conceitos JavaScript Usados

Desestruturação de objeto: { pool }

// O arquivo connection.js exporta um OBJETO com várias propriedades:
module.exports = { pool, testConnection };

// ERRADO - pega o objeto inteiro:
const pool = require('../database/connection');
// pool agora é { pool: ..., testConnection: ... }
// Quando fizer pool.query() → ERRO! O objeto não tem .query()

// CERTO - pega só a propriedade pool:
const { pool } = require('../database/connection');
// pool agora é o pool de conexões MySQL
// pool.query() funciona!
Esse foi o erro que aconteceu na prática! Usamos const pool = require(...) e deu erro 500.
Corrigimos para const { pool } = require(...) e funcionou.
Lição: Sempre verifique o que o arquivo exporta antes de importar.

Desestruturação de array: [rows]

// pool.query() retorna um ARRAY com 2 itens:
// [0] = os dados (rows)
// [1] = metadados (informações sobre as colunas)

// Sem desestruturação:
const resultado = await pool.query('SELECT * FROM leads');
// resultado[0] = dados, resultado[1] = metadados

// Com desestruturação:
const [rows] = await pool.query('SELECT * FROM leads');
// rows = dados (pegou só o primeiro item do array)

async/await - Esperar respostas do banco

// O banco de dados não responde instantaneamente.
// Precisamos ESPERAR a resposta antes de continuar.

// async = "essa função vai ter operações que demoram"
exports.getStats = async (req, res) => {

  // await = "ESPERA essa operação terminar antes de continuar"
  const [rows] = await pool.query('SELECT * FROM leads');
  // Só executa a próxima linha depois que o banco responder

};

try/catch - Tratar erros

try {
  // Tenta executar o código
  const [rows] = await pool.query('SELECT * FROM leads');
  res.json(rows);
} catch (error) {
  // Se der QUALQUER erro, cai aqui
  console.error('Deu ruim:', error);
  res.status(500).json({ error: 'Erro interno' });
}
// Sem try/catch: o servidor CRASHA se der erro
// Com try/catch: o servidor retorna erro 500 e CONTINUA funcionando

|| 0 - Valor padrão

// Quando SUM() não encontra nenhum registro, retorna NULL
// NULL no JSON vira null, e no app pode causar erro

faturamentoMensal: faturamentoPago[0].faturamentoMensal || 0
// Se faturamentoMensal for null → usa 0
// Se faturamentoMensal for 150.00 → usa 150.00
M6 Erros Reais e Como Resolver

Esses erros aconteceram durante a construção. Cada um é uma lição:

Erro 1: Conexão recusada

O que aconteceu: curl: (7) Failed to connect to localhost port 3001
Causa: Testamos na porta 3001, mas o backend roda na porta 3737
Como descobrir a porta certa:
$ ss -tlnp | grep node
# Mostra todas as portas que o Node.js está usando
# Resultado: *:3737 → porta correta é 3737

$ pm2 list
# Mostra os processos e seus PIDs
# Compara o PID do pm2 com o PID do ss para confirmar

Erro 2: Erro 500 (desestruturação)

O que aconteceu: {"error":"Erro ao obter estatísticas"}
Causa: const pool = require('../database/connection')
Solução: const { pool } = require('../database/connection')
Como descobriu: pm2 logs captacliente-api --lines 20 mostrou o erro exato

Erro 3: Erros de SQL (typos)

Erros comuns de digitação: WERE → deveria ser WHERE
campnhas → deveria ser campanhas
financeiro → tabela não existe, a correta é faturas
Faltou aspas em 'ativa'WHERE status = ativa (sem aspas dá erro)
Dica de ouro para SQL: Se a query dá erro, copie ela e teste diretamente no banco (MySQL Workbench, DBeaver, ou terminal mysql). Assim você vê o erro sem o Node no meio.
M7 Resultado Final

Resposta real da API

// GET https://captacliente.techfluxar.com/api/monitor/stats
{
  "totalLeads": 212,
  "leadsByStatus": [
    { "status": "novo", "count": 90 },
    { "status": "contatado", "count": 77 },
    { "status": "respondeu", "count": 45 }
  ],
  "totalUsers": 5,
  "totalCampanhasAtivas": 4,
  "totalCampanhasInativas": 0,
  "faturamentoMensal": 0
}

Arquivos criados

backend/src/ ├── routes/ │ └── monitorRoutes.js ← CRIADO (define GET /monitor/stats) ├── controllers/ │ └── monitorController.js ← CRIADO (consulta banco, monta JSON) └── app.js ← EDITADO (2 linhas adicionadas)

Fluxo completo da requisição

App React Native │ ▼ GET /api/monitor/stats │ ▼ Nginx (captacliente.techfluxar.com) │ rewrite /api/monitor/stats → /monitor/stats ▼ Express (porta 3737) │ app.use('/monitor', monitorRoutes) ▼ monitorRoutes.js │ router.get('/stats', monitorController.getStats) ▼ monitorController.js │ const { pool } = require('../database/connection') │ pool.query('SELECT COUNT(*) ...') ▼ MySQL (banco de dados) │ retorna os dados ▼ res.json({ totalLeads: 212, ... }) │ ▼ App recebe o JSON e exibe na tela
O que você aprendeu construindo isso:
1. Pensar como programador - dividir o problema em partes menores
2. Criar rotas - Express Router, router.get(), module.exports
3. Criar controllers - async/await, try/catch, res.json()
4. Consultar banco - SQL com COUNT, SUM, WHERE, GROUP BY
5. Desestruturação - { pool } e [rows]
6. Conectar rotas - require() e app.use()
7. Debug - pm2 logs, curl, ss -tlnp
8. Resolver erros - ler a mensagem, entender, corrigir
Próximo passo sugerido: Proteger a rota /monitor/stats com uma API Key no header. Assim, somente o seu app React Native consegue acessar os dados. Isso usa o conceito de middleware (capítulo 6 desta apostila).
Parte 2 — WhatsApp na Prática
Evolution API

WhatsApp na prática com o CaptaCliente.
Do Docker ao webhook.

W8 Evolution API — WhatsApp na Prática com CaptaCliente

No módulo anterior você aprendeu a criar um bot standalone com whatsapp-web.js. Agora vamos ver como o CaptaCliente usa WhatsApp de verdade em produção — com a Evolution API, que é uma API REST completa para WhatsApp.

Diferença fundamental
whatsapp-web.js → biblioteca npm, o bot roda dentro do seu código Node.js
Evolution API → serviço separado com API REST. Seu backend faz requisições HTTP para ela. Mais robusto, multi-instância e com dashboard web.

W8.1 Arquitetura — Como o CaptaCliente usa a Evolution API

┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Frontend │────►│ Backend Node │────►│ Evolution API │ │ (React) │◄────│ (Express) │◄────│ (porta 8080) │ └─────────────┘ └──────────────────┘ └────────┬────────┘ │ │ │ Usuário clica evolutionService.js Conecta ao "Conectar WhatsApp" faz fetch() na API WhatsApp real │ │ │ Exibe QR Code Envia mensagens Webhook de na tela via REST respostas

O fluxo funciona assim:

  1. O usuário clica em "Conectar WhatsApp" no frontend React
  2. O frontend faz POST /whatsapp/connect no backend
  3. O backend chama a Evolution API para criar uma instância e gerar o QR Code
  4. O QR Code volta em base64 e é exibido na tela
  5. O usuário escaneia com o celular → WhatsApp conectado
  6. Agora o backend pode enviar mensagens via Evolution API
  7. Quando um lead responde, a Evolution envia um webhook para o backend

W8.2 Instalando a Evolution API no Servidor

A Evolution API roda como um container Docker. Instale no seu servidor (VPS):

Passo 1 — Instalar Docker (se não tiver)

$ curl -fsSL https://get.docker.com | bash
$ docker --version
Docker version 24.x.x

Passo 2 — Subir a Evolution API com Docker

$ docker run -d \
  --name evolution-api \
  -p 8080:8080 \
  -e AUTHENTICATION_API_KEY=SUA_CHAVE_SECRETA_AQUI \
  -v evolution_instances:/evolution/instances \
  atendai/evolution-api
Atenção!
• Substitua SUA_CHAVE_SECRETA_AQUI por uma chave forte (ex: minha-chave-segura-123)
• A flag -v persiste as sessões — se reiniciar o Docker, os WhatsApp continuam conectados
• A porta 8080 é onde a API fica acessível

Passo 3 — Verificar se está rodando

$ curl http://localhost:8080
{"status":200,"message":"Welcome to Evolution API"}

Se aparecer essa mensagem, a Evolution API está rodando! ✅

W8.3 O Service — Encapsulando a Evolution API

No CaptaCliente, toda comunicação com a Evolution está no arquivo evolutionService.js. Ele funciona como uma "ponte" entre o backend e a API:

// backend/src/services/evolutionService.js
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'http://localhost:8080';
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || '';

class EvolutionService {

  // Header de autenticação — todas as requisições usam a apikey
  _headers() {
    return {
      'Content-Type': 'application/json',
      apikey: EVOLUTION_API_KEY,
    };
  }

  // Método genérico para qualquer requisição à Evolution
  async _request(method, path, body) {
    const url = `${EVOLUTION_API_URL}${path}`;
    const options = { method, headers: this._headers() };
    if (body) options.body = JSON.stringify(body);

    const res = await fetch(url, options);
    const data = await res.json();

    if (!res.ok) {
      throw new Error(data.message || `Evolution API erro: ${res.status}`);
    }
    return data;
  }

  // Criar instância (conectar um novo número)
  async createInstance(instanceName) {
    return this._request('POST', '/instance/create', {
      instanceName,
      qrcode: true,
      integration: 'WHATSAPP-BAILEYS',
    });
  }

  // Verificar status da conexão
  async getConnectionStatus(instanceName) {
    return this._request('GET', `/instance/connectionState/${instanceName}`);
  }

  // Gerar QR Code para escanear
  async getQRCode(instanceName) {
    return this._request('GET', `/instance/connect/${instanceName}`);
  }

  // Enviar mensagem de texto
  async sendTextMessage(instanceName, phone, message) {
    return this._request('POST', `/message/sendText/${instanceName}`, {
      number: phone,
      text: message,
    });
  }

  // Enviar com efeito "digitando..." (humanizado)
  async sendTextWithTyping(instanceName, phone, message, delay = 2500) {
    try {
      await this.sendPresence(instanceName, phone, 'composing', delay);
    } catch (_) {}
    await new Promise(r => setTimeout(r, delay));
    return this.sendTextMessage(instanceName, phone, message);
  }

  // Verificar se números têm WhatsApp
  async checkIsWhatsapp(instanceName, phones) {
    return this._request('POST', `/chat/whatsappNumbers/${instanceName}`, {
      numbers: phones,
    });
  }

  // Configurar webhook para receber respostas
  async setWebhook(instanceName) {
    return this._request('POST', `/webhook/set/${instanceName}`, {
      webhook: {
        enabled: true,
        url: `https://seudominio.com/whatsapp/webhook`,
        webhook_by_events: true,
        events: ['MESSAGES_UPSERT'],
      },
    });
  }
}

module.exports = new EvolutionService();
Padrão importante: Service Layer
Toda a lógica de comunicação com a Evolution está encapsulada neste service. O controller nunca faz fetch direto — sempre chama evolutionService.sendTextMessage(), etc. Se amanhã trocar de API, só altera este arquivo.

W8.4 O Controller — Rotas da API WhatsApp

O controller expõe as funcionalidades do WhatsApp como rotas REST que o frontend consome:

Rota Método O que faz
/whatsapp/connect POST Cria instância e retorna QR Code
/whatsapp/status GET Verifica se está conectado
/whatsapp/disconnect POST Desconecta e apaga instância
/whatsapp/send POST Envia mensagem de texto
/whatsapp/send-media POST Envia imagem, PDF, áudio
/whatsapp/check-number POST Verifica se número tem WhatsApp
/whatsapp/webhook POST Recebe respostas (chamado pela Evolution)

Exemplo: Rota de Conectar

// backend/src/controllers/whatsappController.js
async connect(req, res, next) {
  try {
    // 1. Verificar se já tem instância no banco
    let instance = await WhatsappModel.findByUserId(req.user.id);

    // 2. Se já está conectado, retorna direto
    if (instance && instance.status === 'connected') {
      return sendSuccess(res, { status: 'connected' });
    }

    // 3. Nome único da instância (ex: lpp_user_42)
    const instanceName = `lpp_user_${req.user.id}`;

    // 4. Criar na Evolution API (retorna QR Code)
    const qrData = await evolutionService.createInstance(instanceName);

    // 5. Salvar no banco de dados
    await WhatsappModel.create({
      user_id: req.user.id,
      instance_name: instanceName,
      status: 'connecting',
    });

    // 6. Registrar webhook para receber respostas
    await evolutionService.setWebhook(instanceName);

    // 7. Retornar QR Code para o frontend exibir
    sendSuccess(res, {
      qrcode: qrData.base64,
      instanceName,
      status: 'connecting',
    });
  } catch (error) {
    next(error);
  }
}

Exemplo: Rota de Enviar Mensagem

async sendMessage(req, res, next) {
  const { phone, message } = req.body;

  // 1. Normalizar telefone (sempre com 55 na frente)
  let normalizedPhone = phone.replace(/\D/g, '');
  if (!normalizedPhone.startsWith('55')) {
    normalizedPhone = '55' + normalizedPhone;
  }

  // 2. Buscar instância do usuário
  const instance = await WhatsappModel.findByUserId(req.user.id);
  if (!instance || instance.status !== 'connected') {
    return sendError(res, 'WhatsApp não conectado', 400);
  }

  // 3. Verificar se o número tem WhatsApp
  const check = await evolutionService.checkIsWhatsapp(instance.instance_name, normalizedPhone);

  // 4. Enviar com efeito "digitando..."
  const result = await evolutionService.sendTextWithTyping(
    instance.instance_name,
    normalizedPhone,
    message
  );

  // 5. Logar no banco + incrementar contador
  await MensagemLogModel.create({ ... });
  await UserModel.incrementUsage(req.user.id, 'mensagens_enviadas');

  sendSuccess(res, result, 'Mensagem enviada com sucesso');
}

W8.5 Webhook — Recebendo Respostas dos Leads

Quando um lead responde uma mensagem, a Evolution API envia um POST para o endpoint /whatsapp/webhook do seu backend. Esse é o poder do webhook:

Lead responde Evolution API Seu Backend no WhatsApp ──► detecta mensagem ──► /whatsapp/webhook │ │ │ │ POST automático Salva no banco │ com dados da msg Atualiza status │ │ Analisa sentimento │ │ │ ◄──────────────────────────┴──────────────────────────┘ Lead aparece como "respondeu" no CaptaCliente
// backend/src/controllers/whatsappController.js
async webhook(req, res) {
  const { event, instance, data } = req.body;

  // Sempre retorna 200 (senão a Evolution reenvia)
  if (event !== 'messages.upsert') {
    return res.status(200).json({ received: true });
  }

  // Ignorar mensagens enviadas por nós
  if (data.key.fromMe) return res.status(200).json({ received: true });

  // Extrair telefone do remetente
  const phone = data.key.remoteJid.replace('@s.whatsapp.net', '');

  // Buscar lead no banco pelo telefone
  const lead = await LeadModel.findByTelefoneNormalized(userId, phone);

  // Salvar mensagem recebida
  await MensagemRecebidaModel.create({
    user_id: userId,
    lead_id: lead.id,
    telefone: phone,
    mensagem: messageText,
    tipo_mensagem: messageType,  // texto, imagem, audio, video
  });

  // Atualizar status do lead para "respondeu"
  await LeadModel.updateStatus(userId, lead.id, 'respondeu');

  res.status(200).json({ received: true });
}
Importante sobre o Webhook!
• A rota do webhook não tem autenticação JWT — é chamada diretamente pela Evolution API
• Sempre retorne status 200, senão a Evolution reenvia a mesma mensagem várias vezes
• Ignore key.fromMe = true (mensagens que você mesmo enviou)
• Ignore mensagens de grupo (@g.us)

W8.6 Frontend — Página de Conexão WhatsApp

No React, a página de WhatsApp é simples — ela consome as rotas do backend:

// src/pages/WhatsApp.tsx (simplificado)
const [status, setStatus] = useState('disconnected');
const [qrCode, setQrCode] = useState(null);

// Verificar status ao carregar a página
useEffect(() => {
  const checkStatus = async () => {
    const res = await apiFetch('/whatsapp/status');
    setStatus(res.data.status);
  };
  checkStatus();
}, []);

// Conectar — gerar QR Code
const handleConnect = async () => {
  const res = await apiFetch('/whatsapp/connect', { method: 'POST' });
  if (res.data.qrcode) {
    setQrCode(res.data.qrcode);  // base64 da imagem
  }
};

// Exibir QR Code na tela
return (
  <div>
    {qrCode && (
      <img
        src={`data:image/png;base64,${qrCode}`}
        alt="QR Code WhatsApp"
      />
    )}
  </div>
);

W8.7 Configuração — Variáveis de Ambiente

Para tudo funcionar, adicione estas variáveis no seu .env do backend:

# .env do backend
EVOLUTION_API_URL=http://localhost:8080
EVOLUTION_API_KEY=SUA_CHAVE_SECRETA_AQUI
BACKEND_URL=https://seudominio.com
Variável O que é Exemplo
EVOLUTION_API_URL Onde a Evolution está rodando http://localhost:8080
EVOLUTION_API_KEY Chave definida ao subir o Docker minha-chave-123
BACKEND_URL URL pública do backend (para o webhook) https://api.meusite.com

W8.8 Fluxo Completo — Do Clique ao Lead Respondendo

CONECTAR: ┌─────────┐ POST /connect ┌──────────┐ POST /instance/create ┌───────────┐ │ Frontend │ ────────────────► │ Backend │ ──────────────────────► │ Evolution │ │ │ ◄──── QR Code ── │ │ ◄──── QR base64 ─────── │ API │ └─────────┘ └──────────┘ └───────────┘ │ Usuário escaneia com celular → Status muda para "connected" ENVIAR MENSAGEM: ┌─────────┐ POST /send ┌──────────┐ POST /message/sendText ┌───────────┐ │ Frontend │ ────────────────► │ Backend │ ──────────────────────► │ Evolution │ │ │ │ │ (com typing delay) │ API │ └─────────┘ └──────────┘ └─────┬─────┘ │ WhatsApp do lead recebe RECEBER RESPOSTA: ┌───────────┐ POST /webhook ┌──────────┐ │ Evolution │ ────────────────► │ Backend │ → Salva no banco │ API │ │ │ → Atualiza lead p/ "respondeu" └───────────┘ └──────────┘ → Analisa sentimento (IA)

W8.9 Endpoints da Evolution API — Referência Rápida

Ação Método Endpoint
Criar instância POST /instance/create
Gerar QR Code GET /instance/connect/{nome}
Status conexão GET /instance/connectionState/{nome}
Deletar instância DELETE /instance/delete/{nome}
Enviar texto POST /message/sendText/{nome}
Enviar mídia POST /message/sendMedia/{nome}
Verificar número POST /chat/whatsappNumbers/{nome}
Enviar presença POST /chat/sendPresence/{nome}
Configurar webhook POST /webhook/set/{nome}
O que você aprendeu nesta seção:
1. Evolution API — API REST para WhatsApp, roda via Docker na porta 8080
2. Service LayerevolutionService.js encapsula toda comunicação com a Evolution
3. Fluxo de conexão — criar instância → QR Code → escanear → conectado
4. Envio humanizadosendTextWithTyping() simula digitação antes de enviar
5. Webhook — Evolution envia POST quando lead responde, backend salva e atualiza status
6. Verificação de númerocheckIsWhatsapp() verifica se o número tem WhatsApp antes de enviar
7. Frontend simples — React consome as rotas REST, exibe QR Code em base64
8. Variáveis de ambienteEVOLUTION_API_URL, EVOLUTION_API_KEY, BACKEND_URL