Pular para o conteúdo
WhatsApp

WhatsApp: enviar mensagens grátis com a Evolution API

Ilustração colorida de um unicórnio de crina arco-íris operando um terminal flutuante cheio de código, com baús mágicos empilhados como contêineres, uma coruja entregando um pergaminho com QR Code e um castelo com velas ao fundo

Olá meus Unicórnios! 🦄✨

Se você manda mensagem no WhatsApp pela API oficial, tenho uma notícia que provavelmente vai mexer com a sua conta no fim do mês: a partir de 1º de outubro de 2026, a Meta passa a cobrar por mensagem enviada. Não por conversa: por mensagem. 💸

E não é só uma mudança de nome na fatura. As mensagens utilitárias, que eram gratuitas desde julho de 2025 quando enviadas dentro da janela de 24 horas, voltam a ser cobradas. As mensagens de serviço, aquelas respostas que você manda quando o cliente puxa assunto, também entram na conta, com a mesma tarifa das utilitárias.

A documentação oficial da Meta sobre a mudançaOnde eles explicam o que passa a ser cobrado por mensagem a partir de 1º de outubro de 2026.developers.facebook.com

Para quem manda dez mensagens por dia, tudo bem. Para quem tem um sistema que dispara confirmação de pedido, aviso de entrega, lembrete de consulta e código de acesso, a conta muda de patamar, e ela cresce junto com o seu negócio, que é justamente quando dói. 😅

É aí que entra a Evolution API: uma API não oficial, gratuita e de código aberto, que roda no seu servidor e conversa com o WhatsApp Web por baixo dos panos. Você paga a VPS, poucos dólares por mês, e manda quantas mensagens quiser, sem custo por envio.

Este artigo é o passo a passo completo: da VPS Ubuntu recém-comprada, sem nada instalado, até a mensagem saindo pela API. Com as armadilhas que eu encontrei no caminho, e com a conversa honesta sobre o risco, porque essa parte quase ninguém escreve, e ela é a mais importante.

🖥️ O ponto de partida: uma VPS pelada

Usei uma VPS bem modesta de propósito, para provar um ponto: você não precisa de máquina grande para isso. Vou mostrar o consumo real lá no fim do artigo, e ele surpreende.

A minha eu comprei na Contabo, por US$ 5,50 por mês. Foi a conta que fechou melhor para o que eu queria. Veio com Ubuntu 24.04, 8 GB de RAM e 96 GB de disco, que é bem mais do que a Evolution precisa. Serve qualquer provedor, e serve máquina menor também; se você já tem uma VPS parada em algum lugar, ela dá conta.

ContaboOnde eu comprei a VPS deste tutorial, por US$ 5,50 por mês.contabo.com

A primeira coisa é entrar por SSH. No Windows dá para usar o PuTTY, mas o próprio terminal já tem ssh embutido faz anos. Abra o PowerShell e mande:

ssh root@SEU_IP_AQUI

Na primeira conexão ele pergunta se você confia na máquina (aquele Are you sure you want to continue connecting?). Responda yes, digite a senha, e você está dentro.

Antes de instalar qualquer coisa, vale conferir onde você pisou:

cat /etc/os-release | head -3
free -h | head -2
df -h / | tail -1

No meu caso:

PRETTY_NAME="Ubuntu 24.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"

               total        used        free      shared  buff/cache   available
Mem:           7.8Gi       474Mi       7.3Gi       1.0Mi       266Mi       7.3Gi
/dev/sda1        96G  2.2G   94G   3% /

Ubuntu 24.04, 7,8 GB de RAM, 96 GB de disco, e 2,2 GB usados, ou seja, sistema virgem. É desse ponto que a gente parte.

🐳 Instalando o Docker

A Evolution roda em contêiner, então o Docker é a única dependência de verdade. O jeito oficial é o script de conveniência da própria Docker:

apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

Esse script detecta a distribuição sozinho, adiciona o repositório oficial e instala tudo, inclusive o Docker Compose já como plugin, que é o que a gente precisa. Confira as duas versões:

docker --version
docker compose version
Docker version 29.7.2, build a7dcaa6
Docker Compose version v5.5.0

Repare que é docker compose com espaço, e não docker-compose com hífen. O hífen é a versão antiga, em Python, que foi descontinuada. Se você encontrar um tutorial mandando instalar `docker-compose` por fora, é sinal de que ele é velho. 🙃

🔑 Gerando as senhas antes de escrever o arquivo

Aqui vai um hábito que eu recomendo muito: não invente senha na mão e, principalmente, não deixe aquela change-me-123 que vem nos exemplos. A Evolution vai ficar exposta na internet, e a chave dela é a única coisa entre o seu WhatsApp e o mundo.

O próprio Ubuntu gera chaves boas. Primeiro, crie a pasta onde tudo vai morar e entre nela:

mkdir -p /opt/evolution
cd /opt/evolution

O mkdir cria a pasta e o cd entra nela. A partir daqui, todo comando do tutorial roda de dentro de /opt/evolution. Se você fechar o terminal e voltar depois, rode o cd de novo antes de continuar.

Agora as duas senhas, geradas com o openssl, que já vem instalado:

openssl rand -hex 32
openssl rand -hex 16

Cada linha cospe uma sequência aleatória de letras e números, assim:

a3f8e1c7b94d2056fa17e3c8d94b2f60e7a15c39d8b246f0ac91e5d73b820f4e
5d8c1a94f7e30b62d5a8c41f9e26b073

A primeira (a maior) é a chave da API; a segunda é a senha do banco. Copie as duas para um bloco de notas: a primeira você vai usar bastante daqui em diante.

📝 Criando arquivo no Ubuntu: o nano

Se você nunca mexeu num servidor Linux, esta é a parte que trava todo mundo: como é que eu crio um arquivo aqui, se não tem Bloco de Notas? 😅

Tem sim, chama-se nano, já vem instalado, e é o mais simples de todos. Você digita nano seguido do nome do arquivo:

nano .env

A tela do terminal vira o editor. Se o arquivo não existir, ele é criado na hora; se existir, abre para edição. Aí é só digitar (ou colar) o conteúdo normalmente.

Para colar, use Ctrl + Shift + V ou clique com o botão direito. O Ctrl + V comum não funciona no terminal, e essa é a primeira coisa que confunde. 🙃

Para salvar e sair, são três teclas em sequência:

Ctrl + O     grava o arquivo (a letra O, de "output")
Enter        confirma o nome
Ctrl + X     sai do editor

Repare no rodapé do nano: ele mostra os atalhos o tempo todo, com o ^ significando Ctrl. Ou seja, ^O é Ctrl + O e ^X é Ctrl + X. Depois de duas ou três vezes vira automático.

🔐 O arquivo .env, com as senhas

Com o nano na mão, crie o primeiro arquivo:

nano .env

E escreva estas duas linhas, trocando pelos valores que o openssl gerou para você:

AUTHENTICATION_API_KEY=cole_aqui_a_chave_de_64_caracteres
POSTGRES_PASSWORD=cole_aqui_a_senha_de_32_caracteres

Salve com Ctrl + O, Enter, Ctrl + X. Para conferir que ficou certo:

cat .env

O cat mostra o conteúdo de um arquivo na tela. Se aparecerem as duas linhas com as suas senhas, está pronto.

Guarde essa chave num lugar seguro: é ela que você vai usar para entrar no painel e em toda chamada da API. E note que ela fica num arquivo .env separado, não dentro do docker-compose.yml: assim você pode mostrar o compose para alguém sem mostrar a senha junto.

📦 O docker-compose.yml

A Evolution v2 precisa de três peças: ela mesma, um PostgreSQL para guardar as mensagens e um Redis para o cache. Dá para rodar sem banco, mas aí você perde o histórico toda vez que reiniciar, e não vale a pena.

Esse arquivo é a receita que descreve as três: qual imagem cada uma usa, quais portas abre e como conversam entre si. Mesma coisa de antes: nano, cola, salva.

nano docker-compose.yml

E o conteúdo é este (troque o SEU_IP_AQUI pelo IP da sua VPS, aquele mesmo do SSH):

services:
  evolution-api:
    image: evoapicloud/evolution-api:v2.3.7
    container_name: evolution-api
    restart: always
    ports:
      - "8080:8080"
    environment:
      SERVER_URL: http://SEU_IP_AQUI:8080
      AUTHENTICATION_API_KEY: ${AUTHENTICATION_API_KEY}
      DATABASE_ENABLED: "true"
      DATABASE_PROVIDER: postgresql
      DATABASE_CONNECTION_URI: postgresql://evolution:${POSTGRES_PASSWORD}@postgres:5432/evolution
      DATABASE_SAVE_DATA_INSTANCE: "true"
      DATABASE_SAVE_DATA_NEW_MESSAGE: "true"
      DATABASE_SAVE_MESSAGE_UPDATE: "true"
      DATABASE_SAVE_DATA_CONTACTS: "true"
      DATABASE_SAVE_DATA_CHATS: "true"
      CACHE_REDIS_ENABLED: "true"
      CACHE_REDIS_URI: redis://redis:6379/6
      CACHE_REDIS_PREFIX_KEY: evolution
      CACHE_LOCAL_ENABLED: "false"
    depends_on:
      - postgres
      - redis
    volumes:
      - evolution_instances:/evolution/instances

  postgres:
    image: postgres:15-alpine
    container_name: evolution-postgres
    restart: always
    environment:
      POSTGRES_USER: evolution
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: evolution
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: evolution-redis
    restart: always
    volumes:
      - redis_data:/data

volumes:
  evolution_instances:
  postgres_data:
  redis_data:

Salve e saia, como antes. E aqui vai o aviso mais importante para quem está começando:

Antes de subir, dá para perguntar ao próprio Docker se o arquivo está válido. São trinta segundos que evitam muita dor de cabeça:

docker compose config

Se ele imprimir o conteúdo de volta, está tudo certo. Se a indentação estiver errada, você leva um recado assim:

go-yaml load error in scanner at L4.C19: mapping values are not allowed in this context

Traduzindo: ele achou algo fora do lugar na linha 4, coluna 19. A mensagem não diz "faltou um espaço", mas quando esse erro aparecer logo depois de você colar o arquivo, é indentação em 90% das vezes. Confira a linha que ele apontou.

Três coisas nesse arquivo merecem atenção, e uma delas é a armadilha que me custou tempo. Vamos por partes.

💀 A imagem que mudou de nome (e o erro que mente)

Se você só for ler um pedaço deste artigo, leia este. 🙏

Todo tutorial da Evolution que você achar no Google (inclusive o meu antigo) manda usar a imagem atendai/evolution-api. Escrevi exatamente isso na primeira tentativa, e recebi:

Error response from daemon: pull access denied for atendai/evolution-api,
repository does not exist or may require 'docker login'

Repare no que a mensagem sugere: "may require docker login". Ela está te dizendo que talvez seja problema de autenticação. E aí você vai fazer docker login, criar conta no Docker Hub, conferir senha… e nada disso resolve, porque não é esse o problema. 😤

Para confirmar que não era a minha máquina nem a minha rede, testei baixar outra imagem qualquer:

docker pull hello-world
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

Baixou na hora. Ou seja: Docker funciona, rede funciona, DNS funciona. O problema era só com aquele nome.

E a causa é simples quando você descobre: o projeto mudou de organização no Docker Hub. O repositório atendai/evolution-api não existe mais. O endereço novo é:

evoapicloud/evolution-api

É por isso que a mensagem engana tanto. Para o Docker, "repositório que não existe" e "repositório privado que você não pode ver" são indistinguíveis: em ambos os casos ele recebe a mesma negativa do servidor. Então ele junta as duas hipóteses na mesma frase e joga para você decidir. Se ele dissesse "esse repositório não existe", metade da internet teria resolvido isso em dez segundos.

A lição que fica, e vale para qualquer imagem: quando o pull falhar, teste uma imagem que você sabe que existe. Se ela baixar, o problema é o nome, não a sua autenticação.

🚀 Subindo a stack

Com o nome certo no arquivo, é uma linha só:

cd /opt/evolution
docker compose up -d

O -d é de detached: ele sobe e devolve o terminal para você. Na primeira vez demora um pouco, porque precisa baixar tudo. Depois:

docker compose ps
NAME                 STATUS
evolution-api        Up 44 seconds
evolution-postgres   Up 44 seconds
evolution-redis      Up 44 seconds

Os três de pé. Mas não confie no "Up". Ele diz que o contêiner está rodando, não que a aplicação lá dentro terminou de subir. Quem conta a verdade é o log:

docker logs evolution-api | tail -20

O que você quer ver é esta sequência, na ordem:

[Evolution API] v2.3.7  VERBOSE [Redis]              redis ready
[Evolution API] v2.3.7  INFO    [PrismaRepository]   Repository:Prisma - ON
[Evolution API] v2.3.7  LOG     [SERVER]             HTTP - ON: 8080

Redis conectado, banco conectado, e por último o servidor HTTP no ar. Se parar antes do HTTP - ON, tem algo errado, e o log vai dizer o quê, geralmente senha do banco que não bate.

Agora o teste de verdade:

curl http://localhost:8080/
{
    "status": 200,
    "message": "Welcome to the Evolution API, it is working!",
    "version": "2.3.7",
    "clientName": "evolution_exchange",
    "manager": "http://localhost:8080/manager",
    "documentation": "https://doc.evolution-api.com"
}

"it is working!" 🎉 A API está de pé.

🌐 A SERVER_URL: o campo que quebra o painel em silêncio

Olhe de novo a resposta ali de cima, no campo manager:

"manager": "http://localhost:8080/manager"

Isso é um problema, e é sutil. Eu tinha deixado SERVER_URL: http://localhost:8080 no compose, e a Evolution acredita em você: ela usa esse valor para montar todos os links que devolve.

Só que localhost, para quem acessa de fora, é o próprio computador de quem acessa. O painel abre no seu navegador e tenta falar com a API na sua máquina, onde não tem API nenhuma. Resultado: tela em branco, QR Code que não aparece, e nenhuma mensagem de erro explicando o motivo. 🙄

A correção é trocar pelo IP público (ou o domínio, se você tiver um). Abra o compose de novo:

nano docker-compose.yml

Ache a linha do SERVER_URL e deixe-a assim, com o IP da sua VPS:

      SERVER_URL: http://SEU_IP_AQUI:8080

Salve (Ctrl + O, Enter, Ctrl + X) e mande o Docker aplicar a mudança:

docker compose up -d

Não precisa parar nada antes: o up -d percebe sozinho que o arquivo mudou e recria só o contêiner afetado, deixando o banco e o Redis de pé.

Agora a mesma chamada devolve o endereço certo:

{
    "status": 200,
    "message": "Welcome to the Evolution API, it is working!",
    "version": "2.3.7",
    "manager": "http://SEU_IP_AQUI:8080/manager"
}

E tem um detalhe que quase me pegou logo depois. Assim que rodei o docker compose up -d, testei na hora e levei um erro de conexão, e achei que tinha quebrado alguma coisa. Não tinha: recriar o contêiner reinicia a aplicação, e ela leva uns 20 a 30 segundos para regenerar o cliente do banco e voltar a atender. Espere o HTTP - ON aparecer no log antes de concluir que deu errado.

🔐 Provando que a chave protege mesmo

Antes de conectar qualquer WhatsApp, eu quis ter certeza de que a API não estava aberta para o mundo. Testei os três casos:

# 1. Sem chave nenhuma
curl -X POST http://SEU_IP_AQUI:8080/instance/create \
  -H 'Content-Type: application/json' \
  -d '{"instanceName":"invasor"}'

# 2. Com uma chave errada
curl http://SEU_IP_AQUI:8080/instance/fetchInstances \
  -H "apikey: chave-errada-123"

# 3. Com a chave certa
curl http://SEU_IP_AQUI:8080/instance/fetchInstances \
  -H "apikey: SUA_CHAVE_AQUI"

Os dois primeiros bateram na porta e voltaram:

{
    "status": 401,
    "error": "Unauthorized",
    "response": {
        "message": "Unauthorized"
    }
}

E o terceiro passou. É um teste de trinta segundos, mas ele responde a pergunta que importa: a chave está sendo exigida de verdade, e não só decorando o arquivo de configuração.

🔥 Fechando a porta com o firewall

A VPS vem com o firewall desligado, ou seja, toda porta que um contêiner abrir fica exposta. Vamos deixar só as duas que interessam:

ufw allow 22/tcp
ufw allow 8080/tcp
ufw enable

Cuidado com a ordem: libere o 22 antes de ligar o firewall. Se você inverter, o próprio SSH cai junto e você se tranca do lado de fora da máquina. É um erro que só se comete uma vez na vida. 😬

Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     ALLOW IN    Anywhere
[ 2] 8080/tcp                   ALLOW IN    Anywhere
[ 3] 22/tcp (v6)                ALLOW IN    Anywhere (v6)
[ 4] 8080/tcp (v6)              ALLOW IN    Anywhere (v6)

E confira que os dois serviços continuam respondendo depois disso: SSH e API. Firewall que derruba o que devia proteger é pior que firewall nenhum.

🖱️ Entrando no painel

Agora a parte visual. Abra no navegador:

http://SEU_IP_AQUI:8080/manager

O painel pede duas coisas: a Server URL e a API Key Global. E aqui você colhe o fruto de ter arrumado a SERVER_URL lá atrás: o campo já vem preenchido sozinho com o endereço certo. Se ainda estivesse localhost, seria você preenchendo na mão e se perguntando por que não conecta.

A chave é aquela que você gerou com o openssl. Feito o login, aparece a lista de instâncias:

Painel Evolution Manager mostrando a lista de instâncias, com a instância tutorial em status Connecting e a apikey mascarada

Repare no rodapé: Version: 2.3.7, batendo com a imagem que a gente fixou no compose. E na apikey da instância, que o painel mostra mascarada por padrão. Bom sinal, ela não fica exposta na tela para quem passar atrás de você. 👀

📱 Criando a instância e pegando o QR Code

Instância, na Evolution, é cada número de WhatsApp conectado. Dá para criar pelo botão Instance + do painel, mas eu prefiro mostrar pela API, que é o que você vai usar quando for automatizar:

curl -X POST http://SEU_IP_AQUI:8080/instance/create \
  -H "apikey: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"instanceName":"tutorial","integration":"WHATSAPP-BAILEYS","qrcode":true}'

O campo integration é obrigatório na v2 e define qual motor conecta o WhatsApp. O WHATSAPP-BAILEYS é o que usa o WhatsApp Web por trás, que é o caso da maioria. A resposta traz o QR Code já embutido:

instancia: tutorial
status: connecting
hash/apikey: presente
qrcode base64: 12038 bytes
qrcode code: 2@ZcwqBoGsAcg2yKBrcnNAHx0Wm6flYX1RBaIRxc...

Dois detalhes úteis aí. O hash é uma apikey só dessa instância, e dá para entregar ela a um sistema que só pode mexer nesse número, sem passar a chave global. E o qrcode.base64 são 12 KB de imagem pronta, que você pode jogar direto num <img> se estiver montando uma tela própria.

Clicando na instância, o painel mostra o dashboard dela:

Dashboard da instância no Evolution Manager, com o aviso para escanear o QR code, o botão Get QR Code e os contadores de contatos, chats e mensagens zerados

Status Connecting, contadores zerados, e o botão Get QR Code. Clicando nele:

Modal do Evolution Manager exibindo o QR Code do WhatsApp para escanear com o celular

É esse o QR que você lê no celular, em Aparelhos conectados → Conectar um aparelho. Ele expira em poucos segundos e é trocado sozinho. Se demorar para pegar o celular, não se assuste, é só clicar de novo.

E é por isso que aquela SERVER_URL importa tanto: essa tela do QR é exatamente a que fica em branco quando o painel não consegue falar com a API. O QR nem chega a ser desenhado, e não aparece erro nenhum explicando. 🫠

✅ Conectado, e o que acontece nesse instante

Assim que o celular lê o QR, o painel muda para Connected, em verde:

Dashboard do Evolution Manager com a instância em status Connected e os contadores mostrando 564 contatos, 673 chats e 22.042 mensagens sincronizadas

E olhe os contadores, porque tem uma coisa importante aí. Eles pularam de zero para 564 contatos, 673 conversas e 22 mil mensagens em poucos segundos.

Isso não é bug: é o WhatsApp Web funcionando como sempre funcionou. Quando você conecta um aparelho, ele sincroniza o seu histórico. A diferença é que, desta vez, o "aparelho" é um PostgreSQL na sua VPS, e aquelas opções DATABASE_SAVE_DATA_* que a gente ligou lá no compose estão mandando gravar tudo.

Dá para confirmar o estado pela API também, que é como o seu sistema vai perguntar "posso mandar mensagem agora?":

curl "http://SEU_IP_AQUI:8080/instance/connectionState/tutorial" \
  -H "apikey: SUA_CHAVE_AQUI"
{
    "instance": {
        "instanceName": "tutorial",
        "state": "open"
    }
}

O estado open é o que interessa. Enquanto estiver connecting, o QR ainda não foi lido, e todo envio vai falhar.

💬 Enviando a primeira mensagem

Chegamos no motivo de tudo isso. O endpoint é o /message/sendText/ seguido do nome da instância:

curl -X POST "http://SEU_IP_AQUI:8080/message/sendText/tutorial" \
  -H "apikey: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"number":"5511999998888","text":"Olá! Mensagem enviada pela Evolution API. 🦄"}'

O number vai com código do país e DDD, só dígitos. Nada de +, parênteses ou traço. E a resposta vem assim:

{
    "key": {
        "remoteJid": "[email protected]",
        "fromMe": true,
        "id": "3EB035D051A6DC5AAD559A"
    },
    "pushName": "Você",
    "status": "PENDING",
    "message": {
        "conversation": "Olá! Mensagem enviada pela Evolution API. 🦄"
    },
    "messageType": "conversation",
    "messageTimestamp": 1786974398,
    "source": "web"
}

Dois campos merecem explicação, porque assustam à toa.

O status vem PENDING, e isso está certo. Não quer dizer que falhou: quer dizer que a Evolution aceitou a mensagem e a entregou ao WhatsApp, que ainda vai fazer o trabalho dele. É o mesmo relógio que aparece no seu celular antes do tiquinho. Se você esperar "status": "SUCCESS" na resposta do curl, vai esperar para sempre.

O id é o identificador da mensagem no WhatsApp. Guarde-o: é por ele que você vai casar os eventos de entrega e leitura, se um dia ligar webhooks.

Para grupo, muda só o formato do destino. Em vez do número, vai o ID do grupo com o sufixo @g.us:

curl -X POST "http://SEU_IP_AQUI:8080/message/sendText/tutorial" \
  -H "apikey: SUA_CHAVE_AQUI" \
  -H 'Content-Type: application/json' \
  -d '{"number":"[email protected]","text":"Aviso para o grupo. 🚀"}'

🧯 Os dois erros que você vai ver primeiro

Vale conhecer os dois de antemão, porque as mensagens são claras, e isso é raro. 😄

Número que não existe no WhatsApp:

{
    "status": 400,
    "error": "Bad Request",
    "response": {
        "message": [
            {
                "jid": "[email protected]",
                "exists": false,
                "number": "5511999999999"
            }
        ]
    }
}

Esse "exists": false é bem útil: a Evolution checa se o número tem WhatsApp antes de tentar mandar. Ou seja, dá para usar o próprio erro como validação de lista, em vez de sair disparando para número que não existe, o que, aliás, é exatamente o tipo de comportamento que queima a reputação do seu número.

Instância com nome errado:

{
    "status": 404,
    "error": "Not Found",
    "response": {
        "message": [
            "The \"naoexiste\" instance does not exist"
        ]
    }
}

Erro bobo, mas comum: o nome da instância vai na URL, não no corpo do JSON. Trocar o nome sem trocar a URL é engano de todo dia.

🚨 O risco: como não perder o seu número

Chegamos na parte que eu mais queria escrever, e que a maioria dos tutoriais pula. 🙏

A Evolution API funciona automatizando o WhatsApp Web. Não é uma parceria com a Meta, não é um acordo, não é uma brecha autorizada: é o seu servidor se passando por um navegador conectado à sua conta. Isso está fora dos termos de uso, e o WhatsApp tem todo o direito de banir um número que ele considere abusivo.

Quando isso acontece, você não perde só o acesso à API. Você perde o número. As conversas, os grupos, o histórico, o contato que os seus clientes têm salvo há anos. E a chance de recuperar um banimento por automação é baixa.

Então a regra número um, aquela que eu repetiria em negrito no meio de qualquer reunião:

Agora, o que de fato aumenta o risco de bloqueio. Não é usar a API em si: é se comportar como robô. O WhatsApp detecta padrão, não tecnologia.

Disparo em massa para quem não pediu. É o caminho mais rápido para o banimento, sem concorrência. Mandar a mesma mensagem para 500 números comprados numa lista derruba o número em horas. A Evolution não protege você disso: ela obedece.

Número novo já disparando muito. Um chip recém-ativado que manda 300 mensagens no primeiro dia é o retrato do comportamento suspeito. Números têm uma espécie de reputação, e ela se constrói devagar: comece com poucas mensagens por dia e vá subindo ao longo de semanas, no famoso "aquecimento".

Mensagem idêntica repetida. Texto exatamente igual para muita gente é fácil de detectar. Varie: use o nome da pessoa, mude a saudação, alterne a ordem das frases. Fora que a mensagem fica melhor. 😊

Velocidade de robô. Cem mensagens em um minuto não é gente. Coloque um intervalo aleatório entre os envios, de alguns segundos, variando, e não dispare de madrugada.

Bloqueios e denúncias dos usuários. Esse é o sinal mais forte de todos. Quando muita gente bloqueia ou denuncia o seu número, o WhatsApp entende que você está incomodando, e aí não tem técnica que salve. É por isso que a saída sustentável não é disfarçar melhor: é mandar mensagem que a pessoa quer receber.

Resumindo o que dá para levar como prática: use número dedicado, mande só para quem te deu o contato, ofereça uma forma clara de sair, respeite quem pediu para parar, e cresça o volume devagar.

E vale ter na cabeça a comparação honesta, sem torcida: a API oficial custa por mensagem, mais ainda depois de outubro, mas o seu número não corre risco de sumir. A Evolution não custa por mensagem, mas o risco de banimento é seu. Para aviso interno, notificação de sistema, integração pessoal e projeto pequeno, a troca compensa com folga. Para o canal de atendimento do qual a sua empresa depende, pense duas vezes. 🤔

📊 Quanto isso tudo consome, de verdade

Voltando à promessa do começo. Com os três contêineres de pé:

docker stats --no-stream
NAME                 CPU %     MEM USAGE / LIMIT
evolution-api        0.00%     140MiB / 7.755GiB
evolution-postgres   0.00%     39.08MiB / 7.755GiB
evolution-redis      1.86%     3.195MiB / 7.755GiB

Some: 182 MB de RAM para a stack inteira, com a CPU praticamente zerada. Numa máquina de 7,8 GB, isso é 2%.

Ou seja: aquela ideia de que "precisa de uma VPS boa para rodar Evolution" não se sustenta: a menor máquina de qualquer provedor dá conta com folga. O que come recurso não é a API parada, é o volume de mensagens depois.

O disco é outra história, e é aí que a conta pesa:

evoapicloud/evolution-api:v2.3.7   1.83GB
postgres:15-alpine                 417MB
redis:7-alpine                    57.8MB

A imagem da Evolution sozinha tem 1,83 GB. Não é problema de execução: é espaço em disco e tempo de download na primeira vez. Se a sua VPS tem 10 GB, dá, mas não sobra tanto quanto parece.

🔁 Os comandos do dia a dia

Guardando os que eu realmente uso, todos rodados de dentro de /opt/evolution:

docker compose ps          # o que está de pé
docker compose logs -f     # acompanhar os logs ao vivo (Ctrl+C sai)
docker compose restart     # reiniciar sem perder nada
docker compose down        # parar tudo (os dados ficam nos volumes)
docker compose up -d       # subir de novo

Uma diferença que vale gravar: docker compose down remove os contêineres mas preserva os volumes, então suas instâncias e mensagens continuam lá quando você subir de novo. Já docker compose down -v, com o -v, apaga os volumes junto, e aí some tudo, inclusive as sessões de WhatsApp conectadas. Esse -v é pequenininho e faz um estrago enorme. ⚠️

Para atualizar a Evolution quando sair versão nova, troque a tag no compose e:

docker compose pull
docker compose up -d

🔒 Duas coisas antes de usar isso para valer

O que está aqui funciona, mas roda em HTTP puro, na porta 8080. Para um teste está ótimo; para uso real, duas mudanças são importantes.

A primeira é colocar HTTPS na frente, com um domínio e um proxy reverso. Sem isso, a sua apikey viaja em texto puro em toda requisição, e qualquer um no caminho da rede consegue lê-la.

A segunda é não deixar a 8080 aberta para o mundo. Se só o seu servidor de aplicação vai conversar com a Evolution, restrinja o firewall ao IP dele em vez de liberar para todos:

ufw delete allow 8080/tcp
ufw allow from IP_DO_SEU_APP to any port 8080

São dois assuntos que dão artigo próprio, e eu não vou enfiar aqui só para dizer que falei. Mas fica o aviso: uma Evolution aberta na internet com HTTP é uma porta para o seu WhatsApp, e a chave é tudo que protege ela.

No fim das contas, a conta que fecha é essa: US$ 5,50 por mês de VPS contra uma tarifa por mensagem que só cresce, pesando contra o risco de o número ir embora um dia. Sabendo desse risco e escolhendo o número certo para correr ele, a Evolution resolve muito bem a vida de quem só quer o sistema avisando as coisas no WhatsApp sem pagar pedágio por aviso. 🦄

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟

Perguntas frequentes

A Evolution API é gratuita?
A Evolution API é de código aberto e não cobra por mensagem enviada. Você paga apenas o servidor onde ela roda: a VPS usada neste tutorial custa US$ 5,50 por mês. É essa a diferença em relação à API oficial da Meta, que a partir de 1º de outubro de 2026 passa a cobrar por mensagem enviada, incluindo as utilitárias e as de serviço.
Usar a Evolution API pode banir meu número do WhatsApp?
Sim, o risco é real. A Evolution API automatiza o WhatsApp Web, o que está fora dos termos de uso do WhatsApp, e um banimento faz perder o número inteiro: conversas, grupos e histórico. Por isso nunca conecte o número principal do negócio numa primeira instalação: use um chip novo e dedicado, que você possa perder sem que nada pare.
O que aumenta o risco de bloqueio do número?
Não é usar a API: é se comportar como robô, porque o WhatsApp detecta padrão, não tecnologia. Os fatores que mais pesam são disparo em massa para quem não pediu, número novo já enviando muito volume, mensagem idêntica repetida para muita gente, velocidade de envio sem intervalo, e principalmente bloqueios e denúncias dos usuários, que é o sinal mais forte de todos.
Que servidor é necessário para rodar a Evolution API?
Bem menos do que se costuma dizer. Com os três contêineres de pé (API, PostgreSQL e Redis), o consumo medido com docker stats foi de 182 MB de RAM no total e CPU praticamente zerada. O que exige atenção é o disco: só a imagem da Evolution tem 1,83 GB, e com PostgreSQL e Redis o conjunto passa de 2,3 GB.
Qual a diferença entre a Evolution API e a API oficial do WhatsApp?
A API oficial cobra por mensagem, mais ainda depois de outubro de 2026, mas o número não corre risco de ser banido. A Evolution não cobra por envio, e o risco de banimento é seu. Para aviso interno, notificação de sistema e projeto pequeno, a troca compensa com folga; para o canal de atendimento do qual a empresa depende, vale pensar duas vezes.
O comando <code>docker compose down</code> apaga as instâncias conectadas?
Não. O docker compose down remove os contêineres mas preserva os volumes, então as instâncias e as mensagens continuam lá quando você subir de novo. O que apaga tudo é o docker compose down -v: o -v remove os volumes junto, incluindo as sessões de WhatsApp já conectadas.

Leia também