n8n + EvolutionAPI + OpenAI · full stack
Complete tutorial to get an AI agent answering on WhatsApp in one afternoon, from the server to your first lead, with n8n, EvolutionAPI and OpenAI.
The n8n + EvolutionAPI + OpenAI combination is the most widely used stack in Brazil for AI agents on WhatsApp. This guide takes you from a freshly provisioned server to your first answered lead, without skipping the steps that tend to break in production.
TL;DR
- Total time: ~2 hours (without distractions)
- Server cost: from R$ 79/month (VPS Pro)
- OpenAI cost: ~R$ 0,04 per 10-message conversation (gpt-4o-mini)
- Result: WhatsApp replying automatically based on whatever you define
What you will need
Prerequisites
- A VPS with Docker (we recommend VPS Pro)
- Your own domain (
automacao.sua-empresa.com.br) - An OpenAI token (or Anthropic Claude, the setup is the same)
- A non-personal WhatsApp number (use a dedicated SIM)
Architecture
WhatsApp ──▶ EvolutionAPI ──webhook──▶ n8n ──▶ OpenAI ──▶ reply
▲ │
└──── message sending ───────────────────────┘
Each piece runs in its own Docker container, behind a single reverse proxy (Caddy or Traefik) that handles HTTPS.
Step 1: provision the VPS
After ordering the server, you receive a welcome email with the address, SSH port, username and password. Use exactly the values from the email.
Once connected, create a non-root user and install Docker:
adduser deploy
usermod -aG sudo deploy
su - deploy
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker deploy
exit
Log back in as deploy (same command from the email, changing the user):
docker --version
Step 2: point DNS
At your registrar (Registro.br, Cloudflare, etc.), create 2 subdomains pointing to the VPS IP:
| Type | Name | Value |
|---|---|---|
| A | n8n | VPS IP |
| A | evo | VPS IP |
Allow up to 30 minutes for propagation. Confirm with:
dig n8n.sua-empresa.com.br +short
dig evo.sua-empresa.com.br +short
Both should return the VPS IP.
Step 3: docker-compose
Create the project folder:
mkdir -p ~/stack-ia && cd ~/stack-ia
Generate an encryption key for n8n:
openssl rand -hex 32
Save the output: you will put it in N8N_ENCRYPTION_KEY.
Create ~/stack-ia/.env:
DOMAIN_BASE=sua-empresa.com.br
N8N_ENCRYPTION_KEY=cole_aqui_o_hex_de_64_caracteres
EVOLUTION_API_KEY=defina_uma_string_secreta_grande
POSTGRES_PASSWORD=outra_string_secreta
Create ~/stack-ia/docker-compose.yml:
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: rollin
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: postgres
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "rollin"]
interval: 10s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
evolution-api:
image: atendai/evolution-api:v2
restart: unless-stopped
environment:
AUTHENTICATION_API_KEY: ${EVOLUTION_API_KEY}
DATABASE_ENABLED: "true"
DATABASE_PROVIDER: postgresql
DATABASE_CONNECTION_URI: postgresql://rollin:${POSTGRES_PASSWORD}@postgres:5432/evolution?schema=public
DATABASE_SAVE_DATA_INSTANCE: "true"
DATABASE_SAVE_DATA_NEW_MESSAGE: "true"
DATABASE_SAVE_MESSAGE_UPDATE: "true"
CACHE_REDIS_ENABLED: "true"
CACHE_REDIS_URI: redis://redis:6379/2
SERVER_URL: https://evo.${DOMAIN_BASE}
depends_on:
postgres:
condition: service_healthy
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
environment:
N8N_HOST: n8n.${DOMAIN_BASE}
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.${DOMAIN_BASE}/
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_USER: rollin
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
DB_POSTGRESDB_DATABASE: n8n
EXECUTIONS_PROCESS: main
GENERIC_TIMEZONE: America/Sao_Paulo
depends_on:
postgres:
condition: service_healthy
volumes:
pg_data:
caddy_data:
caddy_config:
Create ~/stack-ia/Caddyfile:
n8n.sua-empresa.com.br {
reverse_proxy n8n:5678
}
evo.sua-empresa.com.br {
reverse_proxy evolution-api:8080
}
Replace sua-empresa.com.br with your real domain.
Create the databases in Postgres before bringing everything up:
docker compose up -d postgres
sleep 10
docker compose exec postgres psql -U rollin -c "CREATE DATABASE n8n;"
docker compose exec postgres psql -U rollin -c "CREATE DATABASE evolution;"
Bring up everything else:
docker compose up -d
docker compose logs -f
Wait for Caddy to issue SSL (30-60s). Then open:
https://n8n.sua-empresa.com.br→ n8n signup screenhttps://evo.sua-empresa.com.br→ JSON{"status":200,...}
Step 4: create the instance in EvolutionAPI
curl -X POST https://evo.sua-empresa.com.br/instance/create \
-H "Content-Type: application/json" \
-H "apikey: SEU_EVOLUTION_API_KEY" \
-d '{
"instanceName": "atendimento",
"qrcode": true,
"integration": "WHATSAPP-BAILEYS"
}'
The response includes qrcode.base64. Decode it and scan it in WhatsApp:
echo "BASE64_AQUI" | base64 -d > qr.png
Download qr.png to your machine and open it. In WhatsApp on your phone: Linked devices → Link a device and scan it.
Confirm it connected:
curl https://evo.sua-empresa.com.br/instance/connectionState/atendimento \
-H "apikey: SEU_EVOLUTION_API_KEY"
You should see "state":"open".
Step 5: workflow in n8n
Go to https://n8n.sua-empresa.com.br, create your admin account and follow the steps below.
5.1: add the Webhook node
New workflow → Webhook with:
- HTTP Method:
POST - Path:
whats-in-atendimento - Response Mode:
Immediately
Save and copy the Production URL (something like https://n8n.sua-empresa.com.br/webhook/whats-in-atendimento).
5.2: configure the webhook in EvolutionAPI
curl -X POST https://evo.sua-empresa.com.br/webhook/set/atendimento \
-H "Content-Type: application/json" \
-H "apikey: SEU_EVOLUTION_API_KEY" \
-d '{
"webhook": {
"enabled": true,
"url": "https://n8n.sua-empresa.com.br/webhook/whats-in-atendimento",
"events": ["MESSAGES_UPSERT"],
"webhookByEvents": false
}
}'
Send a test message to the connected number and watch it arrive in n8n. The payload carries the text in body.data.message.conversation.
5.3: IF filter
Add an IF node with:
- Condition 1:
{{ $json.body.data.key.fromMe }}isfalse(ignores messages sent by you) - Condition 2:
{{ $json.body.data.message.conversation }}is not empty
5.4: OpenAI Chat Model node
- Credential: create it with your OpenAI token
- Model:
gpt-4o-mini - Messages:
System: Você é o atendente da Rollin Host. Responda em português,
em 2-3 frases curtas. Se a pergunta for sobre preço, redirecione para
https://www.rollinhost.com.br/calculadora-economia-com-ia
User: {{ $json.body.data.message.conversation }}
5.5: HTTP Request node to send the reply
- Method:
POST - URL:
https://evo.sua-empresa.com.br/message/sendText/atendimento - Headers:
apikey:SEU_EVOLUTION_API_KEYContent-Type:application/json
- Body (JSON):
{
"number": "{{ $('Webhook').item.json.body.data.key.remoteJid }}",
"text": "{{ $json.choices[0].message.content }}"
}
5.6: activate
Activate the workflow (toggle in the top right corner).
Step 6: test end to end
Send a message to the connected WhatsApp number. Within ~3-5 seconds, you receive the generated reply.
If it does not work, debug in this order:
- Did EvolutionAPI receive it? →
docker compose logs evolution-api - Did the webhook reach n8n? → check Executions in n8n (it should show up)
- Did OpenAI respond? → open the execution and check the OpenAI node payload
- Did sending fail? → check the HTTP Request status: if it is
400, thenumberis wrong (it must include area code + country code without+, ending in@s.whatsapp.netif used directly)
Step 7: production, keep this in mind
How the cost behaves
Per typical conversation (10 messages, ~500 input tokens + 200 output with gpt-4o-mini):
- Input: 500 × R$ 0,00075 / 1k = R$ 0,00038
- Output: 200 × R$ 0,003 / 1k = R$ 0,0006
- Total per conversation: ~R$ 0,001
At 1.000 conversations/month = R$ 1. The VPS is most of the cost.
Next steps
Connect more numbers
Swap OpenAI for your own LLM
Add memory to the agent
Scale to high volume
Last updated: