🤔 O que é Evolution API e por que escolhemos
Evolution API é wrapper open-source em volta do Baileys (cliente WhatsApp Web não-oficial). Você sobe num Docker, conecta um número escaneando QR, e tem WhatsApp programável em minutos — sem a burocracia da Cloud API oficial da Meta.
| Critério | Evolution API | WhatsApp Cloud (Meta) |
|---|---|---|
| Tempo até funcionar | 10 min | 2 a 6 semanas |
| Aprovação Meta / BSP | não precisa | obrigatória |
| Custo por mensagem | R$ 0 | R$ 0,02–0,40 |
| Conformidade ToS | cinza | oficial |
| Risco de banimento | médio | zero |
| Templates aprovados | livre | só template aprovado |
| Recomendado pra | PME, MVP, atendimento | grande volume, marketing |
📌 O trade-off honesto
Evolution não é "production-grade enterprise". Existe risco do número ser bloqueado se você fizer disparo em massa indevido. Pra atendimento (cliente fala primeiro, você responde), o risco é baixíssimo. Pra PME brasileira de até 5k mensagens/mês, é a escolha vencedora hoje — e quando o cliente crescer pra 100k/mês, migra pra Cloud API mantendo o mesmo backend.
☁️ Self-host vs cloud
Você pode rodar Evolution na sua própria VPS por R$ 30/mês ou pagar um serviço gerenciado. Margem do seu SaaS depende dessa decisão.
🖥️ Self-host (VPS)
- ✓Controle total: backup, logs, atualização
- ✓Margem alta — sustenta 5 a 10 clientes na mesma VPS
- ✓Sem dependência de fornecedor que pode mudar regra
- !Você responde plantão se cair de madrugada
- !Precisa configurar SSL, firewall, backup
☁️ Provedor SaaS
- ✓Sem dor de operação — sobe e usa
- ✓Suporte e SLA contratual
- !Margem baixa — não sustenta cliente de R$ 200/mês
- !Lock-in: trocar depois exige reescrever integração
- ✗Cliente perceptivelmente mais devagar
💡 A recomendação prática
Comece com SaaS pago no primeiro cliente pra validar produto. Quando chegar no terceiro cliente pagante, migre pra self-host. O ponto de equilíbrio é 3 clientes — abaixo disso, paga o cloud e foca em vender; acima, opera pra capturar margem.
🐳 Setup local com Docker
Um arquivo, um comando, dez minutos. docker-compose up sobe Evolution + Postgres + Redis e expõe um painel web em http://localhost:8080.
📄 docker-compose.yml
YAMLversion: "3.9"
services:
evolution:
image: atendai/evolution-api:latest
container_name: evolution_api
restart: always
ports:
- "8080:8080"
environment:
AUTHENTICATION_API_KEY: ${EVOLUTION_API_KEY}
DATABASE_ENABLED: "true"
DATABASE_PROVIDER: postgresql
DATABASE_CONNECTION_URI: postgresql://evo:${DB_PASS}@postgres:5432/evolution
REDIS_ENABLED: "true"
REDIS_URI: redis://redis:6379
WEBHOOK_GLOBAL_URL: ${WEBHOOK_URL}
WEBHOOK_GLOBAL_ENABLED: "true"
QRCODE_LIMIT: "30"
depends_on:
- postgres
- redis
volumes:
- evolution_instances:/evolution/instances
postgres:
image: postgres:15-alpine
restart: always
environment:
POSTGRES_USER: evo
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: evolution
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U evo"]
interval: 10s
redis:
image: redis:7-alpine
restart: always
volumes:
- redis_data:/data
volumes:
evolution_instances:
postgres_data:
redis_data:
📄 .env
bashEVOLUTION_API_KEY=troque-isso-por-uuid-forte DB_PASS=outro-uuid-forte WEBHOOK_URL=https://seu-app.convex.site/webhooks/evolution
🚀 Subir e validar
# subir docker compose up -d # verificar saúde curl http://localhost:8080/ # logs em tempo real docker compose logs -f evolution # parar tudo docker compose down # reset completo (apaga sessões) docker compose down -v
⚠️ Uma armadilha comum
Webhook usa localhost dá pau porque o container não enxerga o seu host. Use ngrok em desenvolvimento (ngrok http 3000) e coloque a URL HTTPS retornada como WEBHOOK_URL. Em produção, é a URL da Vercel ou Convex.
📲 Conectando WhatsApp pessoal
Cada cliente tem uma "instância" no Evolution. Você cria, mostra QR code na sua UI, ele escaneia, e a sessão fica salva. Cair e reconectar deve ser invisível pro cliente.
🔄 Ciclo de vida de uma instância
POST /instance/create { instanceName: "cliente_abc" }
GET /instance/connect/cliente_abc
GET /instance/connectionState/cliente_abc
📋 Boas práticas pra não cair
🔔 Webhooks: receber mensagens
Webhook é o coração do produto. Toda vez que chega mensagem no WhatsApp do cliente, Evolution dá POST no seu endpoint. Você valida, salva no banco e dispara o agente IA.
🌊 Fluxo do webhook
📄 convex/http.ts (handler do webhook)
TypeScriptimport { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";
const http = httpRouter();
http.route({
path: "/webhooks/evolution",
method: "POST",
handler: httpAction(async (ctx, req) => {
// 1. valida segredo no header
const auth = req.headers.get("apikey");
if (auth !== process.env.EVOLUTION_API_KEY) {
return new Response("unauthorized", { status: 401 });
}
const body = await req.json();
if (body.event !== "messages.upsert") return new Response("ok");
const msg = body.data;
if (msg.key.fromMe) return new Response("ok"); // ignora as nossas
// 2. idempotência: se já processei este id, sai
const existing = await ctx.runQuery(internal.messages.byExternalId, {
externalId: msg.key.id,
});
if (existing) return new Response("ok");
// 3. resolve contato e conversa
const phone = msg.key.remoteJid.split("@")[0];
const { contactId, conversationId } = await ctx.runMutation(
internal.contacts.upsertAndOpen,
{ instanceName: body.instance, phone, name: msg.pushName }
);
// 4. grava mensagem
await ctx.runMutation(internal.messages.create, {
conversationId,
externalId: msg.key.id,
direction: "inbound",
sender: "contact",
body: msg.message?.conversation ?? "[mídia]",
sentAt: msg.messageTimestamp * 1000,
});
// 5. agenda agente IA
await ctx.scheduler.runAfter(0, internal.ai.respond, { conversationId });
return new Response("ok");
}),
});
export default http;
📌 Os 3 pecados capitais do webhook
1) Não validar idempotência — Evolution reentrega em retry e sua UI mostra mensagem duplicada. 2) Processar tudo no handler — bloqueia, dá timeout, perde mensagem. Use scheduler.runAfter(0, ...). 3) Engolir erro silenciosamente — sempre logue com nível ERROR e dispare alerta. Mensagem perdida é a queixa nº 1 de cliente nesse domínio.
📤 Enviando mensagens
Texto, imagem, áudio, document. Cada tipo tem seu endpoint, mas a função pode ser uma só com switch — facilita usar do agente IA.
📄 convex/lib/whatsapp.ts
TypeScripttype SendPayload =
| { kind: "text"; text: string }
| { kind: "image"; url: string; caption?: string }
| { kind: "audio"; url: string }
| { kind: "document"; url: string; filename: string };
export async function sendMessage(
instanceName: string,
to: string,
payload: SendPayload
) {
const base = process.env.EVOLUTION_BASE_URL!;
const headers = {
"Content-Type": "application/json",
apikey: process.env.EVOLUTION_API_KEY!,
};
const number = to.replace(/\D/g, "") + "@s.whatsapp.net";
const routes = {
text: { path: "sendText", body: { number, text: (payload as any).text } },
image: { path: "sendMedia", body: { number, mediatype: "image",
media: (payload as any).url, caption: (payload as any).caption } },
audio: { path: "sendWhatsAppAudio", body: { number, audio: (payload as any).url } },
document: { path: "sendMedia", body: { number, mediatype: "document",
media: (payload as any).url, fileName: (payload as any).filename } },
} as const;
const route = routes[payload.kind];
const res = await fetch(`${base}/message/${route.path}/${instanceName}`, {
method: "POST", headers, body: JSON.stringify(route.body),
});
if (!res.ok) throw new Error(`evolution ${res.status}: ${await res.text()}`);
return res.json();
}
📄 Exemplo de payload JSON (envio de texto)
JSON{
"number": "5511987654321@s.whatsapp.net",
"text": "Olá Ana, posso te confirmar a reunião amanhã às 14h?"
}
⚖️ Limites e boas práticas
- • Máx 1 msg/segundo por instância
- • Pausa de 3–8s entre mensagens em sequência
- • Atrasinho aleatório imita "digitando"
- • Até 5k msgs/dia em número aquecido
- • Imagem: até 5MB, JPEG/PNG
- • Áudio: ogg/opus pra "voice note"
- • Documento: até 100MB, PDF preferido
- • URL pública preferida sobre base64
💡 Áudio é o segredo brasileiro
Cliente brasileiro adora áudio do WhatsApp. Use TTS (ElevenLabs ou OpenAI) pra gerar resposta em ogg/opus quando o lead também mandou áudio. Taxa de resposta sobe 40% versus texto puro. Detalhe que faz o produto parecer humano sem ser.
✅ O que Aprendemos
Próximo Módulo:
4.3 — Agente IA dentro do produto: system prompt do qualificador, function calling pra criar deals, handoff humano e otimização de custo via prompt caching.