Self-Host a Free WhatsApp API on Your Own Machine with n8n and OpenWA
This is the single-page version of a 6-part series. Every step is here — read top to bottom and you will have a working WhatsApp AI bot running on your own machine by the end.
Table of Contents
Prerequisites
~/projects as the workspacePart 1 — n8n + Cloudflare Tunnel Setup
Read the dedicated post: n8n + Cloudflare Tunnel Setup
Step 1: Directory and Docker Compose
Create the project directory:
mkdir -p ~/projects/n8n
cd ~/projects/n8n
Create docker-compose.yml:
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
ports:
- "5679:5678"
environment:
- N8N_HOST=n8n.yourdomain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_EDITOR_BASE_URL=https://n8n.yourdomain.com
- WEBHOOK_URL=https://n8n.yourdomain.com/
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem
- N8N_PROXY_HOPS=1
- N8N_RUNNERS_ENABLED=true
- GENERIC_TIMEZONE=UTC
volumes:
- n8n_data:/home/node/.n8n
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5678/healthz"]
interval: 30s
timeout: 10s
retries: 5
volumes:
n8n_data:
driver: local
networks:
default:
name: openwa-network
Replace n8n.yourdomain.com with your actual subdomain throughout.
Important — the shared Docker network: Both n8n and OpenWA must be on the same Docker network so n8n can reach OpenWA at http://openwa-api:2785 by container name. The compose above creates a network named openwa-network. When you bring up OpenWA in Part 2, make sure its compose also joins this network — add the following to your docker-compose.dev.yml under OpenWA:
networks:
default:
name: openwa-network
external: true
If they are on separate networks, n8n will get connection refused when trying to send WhatsApp messages — and the error won't tell you why.
Follow the Cloudflare Tunnel Setup guide to install cloudflared, create the tunnel, configure ingress rules, and route your domain.
Once the tunnel is live and n8n is reachable at https://n8n.yourdomain.com, continue below.
n8nctl — a simple script to start/stop n8n and the Cloudflare tunnel together. Get it here.
Part 2 — OpenWA Docker Setup
Read the dedicated post: OpenWA Docker Setup — From Zero to First Login
OpenWA is an open-source WhatsApp API gateway built on NestJS and whatsapp-web.js. It gives you a REST API to send and receive messages without paying Meta for WhatsApp Business API access.
GitHub: github.com/rmyndharis/OpenWA
Clone and One Command
cd ~/projects
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
docker compose -f docker-compose.dev.yml up -d
No environment files to create. The dev compose has zero-config defaults for everything: NestJS API + dashboard on port 2785, SQLite, Chromium headless, named volume for all data, ports bound to 127.0.0.1.
Before you start — append this to the bottom of docker-compose.dev.yml in the OpenWA directory so it joins the same network as n8n:
networks:
default:
name: openwa-network
Then run the compose command above. Without this, n8n cannot reach OpenWA by container name and you will get silent connection refused errors.
Wait for It to Start
docker compose -f docker-compose.dev.yml logs -f
Look for OpenWA is running on http://localhost:2785. Takes 10–20 seconds on first run.
Login to the Dashboard
Open http://localhost:2785. The first-boot screen prompts for the admin API key.
# Find the container name
docker ps --format '{{.Names}}'
# Read the key
sudo docker exec container-name cat /app/data/.api-key
Paste the key into the dashboard. Store it in a password manager — you need it for every API call and for n8n credentials.
Session State Machine
disconnected → connecting → authenticating (scan QR) → ready
Stuck at "authenticating"? Pin a known-good WhatsApp Web version in .env:
WWEBJS_WEB_VERSION=2.3000.1023204257
Part 3 — Connecting n8n to OpenWA
Read the dedicated post: Connecting n8n to OpenWA — First Integration
n8n and OpenWA run on the same machine. OpenWA never needs a public URL. n8n → OpenWA is always localhost:2785. OpenWA → n8n (webhooks) requires your public Cloudflare Tunnel URL.
Step 1: Register the Webhook in OpenWA
Go to http://localhost:2785 → Webhooks → Create Webhook.
Fill in:
default)https://n8n.yourdomain.com/webhook-test/ (test) or https://n8n.yourdomain.com/webhook/ (production)Tip: Register the test URL first (/webhook-test/). Use the test URL while building and debugging your workflow — it doesn't require authentication. Once everything works, switch to the production URL (/webhook/).
Available events:
| Event | When it fires |
|---|---|
message.received |
A message arrives on your WhatsApp |
message.sent |
A message is sent from your session |
message.ack |
Message delivery/read acknowledgement |
message.failed |
Message failed to send |
message.revoked |
A message was deleted by sender |
session.status |
Session status changes |
session.qr |
QR code generated (scan required) |
session.authenticated |
Session authenticated successfully |
session.disconnected |
Session disconnects |
group.join |
Someone joins a group |
group.leave |
Someone leaves a group |
group.update |
Group info updated |
* |
All events |
For a basic bot, select message.received only. Adding * fires on everything — including your own outgoing messages, which will cause an infinite loop if your workflow replies unconditionally.
Step 2: Add the OpenWA Credential in n8n
Open https://n8n.yourdomain.com → Credentials → Add Credential → HTTP Header Auth:
X-API-KeyBefore Step 3: Open your n8n Webhook node and click Listen. Then send a message from WhatsApp (or have someone message you). n8n will receive the payload. If you see the incoming data in the Webhook node's output — you are connected. Proceed to Step 3.
Step 3: Configure the HTTP Request Node
Add an HTTP Request node in your n8n workflow:
http://localhost:2785/api/send/text
{
"session": "default",
"to": "{{ $json.message.from }}",
"text": "Got your message!"
}
Part 4 — WhatsApp + n8n Workflow Varieties
Read the dedicated post: 3 WhatsApp + n8n Workflow Varieties
Three workflows from the same webhook trigger. All use: OpenWA at http://openwa-api:2785 (Docker network hostname, not localhost), Ollama llama3.2:3b for AI, 30-message Simple Memory per chatId.
Variety 1: Personal AI Chat Bot
IF node fires when body.data.contact.pushName equals the name you set. Everyone else is silently ignored.
Three mistakes:
Variety 2: Group @agent Bot
The bot lives in a WhatsApp group. Someone @mentions it to get a response — without the mention filter, every message in the group fires the workflow and the bot replies to its own replies → infinite loop.
How it works:
body.data.isGroup == true AND message contains "@agent"Two mistakes to avoid:
Variety 3: Keyword Router
Avoids spam entirely — no @mention needed, but the bot only reacts to specific keywords. This is the cleanest production setup for group commands.
How it works:
@order, @help, @status, etc.)Why this avoids spam:
The Workflow JSON
Copy the JSON below → In n8n: Workflows → three dots menu → Import from JSON → paste → import. Then re-select credentials in every node with a key icon.
{
"name": "WhatsApp AI Bot — 3 Varieties",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "wa-webhook",
"options": {}
},
"id": "321adc8a-c16f-4b60-baa3-3c51d7f748d6",
"name": "WA Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [112, 544],
"webhookId": "49cae27c-b557-4d0c-a7ab-101a3d83c8ac"
},
{
"parameters": {
"method": "POST",
"url": "=http://openwa-api:2785/api/sessions/{{ $('WA Webhook').item.json.body.sessionId }}/messages/send-text",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [{ "name": "Content-Type", "value": "=application/json" }]
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{ "name": "chatId", "value": "={{ $json.chatId }}" },
{ "name": "text", "value": "={{ $json.output }}" }
]
},
"options": {}
},
"id": "52d1f993-69fb-461b-abf6-e683c288e505",
"name": "Send WhatsApp Message",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [1632, 544],
"credentials": { "httpHeaderAuth": { "id": "REPLACE_ME", "name": "OpenWA API Key" } }
},
{
"parameters": { "model": "llama3.2:3b", "options": {} },
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [1008, 784],
"id": "f731fc8d-6965-45a3-82bc-0ffe634a1173",
"name": "Ollama Chat Model",
"credentials": { "ollamaApi": { "id": "REPLACE_ME", "name": "Ollama account" } }
},
{
"parameters": {
"sessionIdType": "customKey",
"sessionKey": "={{ $json.body.data.chatId }}",
"contextWindowLength": 30
},
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"typeVersion": 1.4,
"position": [1184, 816],
"id": "d29aeafc-0398-477c-9757-e0cb25799ac4",
"name": "Simple Memory"
},
{
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 },
"conditions": [{
"id": "6c1bd14c-82d9-4e84-9710-f7cff64ff5e7",
"leftValue": "={{ $json.body.data.contact.pushName }}",
"rightValue": "YourContactName",
"operator": { "type": "string", "operation": "equals" }
}],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [544, 288],
"id": "5d673758-ca77-4362-88b8-4f1c7c657332",
"name": "Variety 1: Personal Contact"
},
{
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 },
"conditions": [{
"id": "447c3290-2b09-4bd5-9a20-87373df3dc3d",
"leftValue": "={{ $json.body.data.isGroup }}",
"rightValue": true,
"operator": { "type": "boolean", "operation": "equals" }
}],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [560, 512],
"id": "360ee92f-467a-4feb-85c5-7688834a3b1b",
"name": "Variety 2: Group Message"
},
{
"parameters": {
"conditions": {
"options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 3 },
"conditions": [
{
"id": "424bfcff-ef4a-4911-bdea-44eb2c4cc52e",
"leftValue": "={{ $json.body.data.isGroup }}",
"rightValue": true,
"operator": { "type": "boolean", "operation": "equals" }
},
{
"id": "f60b4be0-1c7b-4ecd-bf65-2b3124897963",
"leftValue": "={{ $json.body.data.body }}",
"rightValue": "@agent",
"operator": { "type": "string", "operation": "contains" }
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [560, 736],
"id": "b9545930-1ec0-435b-8d9a-3c757ac35f17",
"name": "Variety 3: @agent Mention"
},
{
"parameters": {
"assignments": {
"assignments": [
{ "id": "be20df00-04bc-46aa-8f26-462af1db0dcb", "name": "output", "value": "={{ $json.output }}", "type": "string" },
{ "id": "c58843cc-e1ec-4803-97c0-b3dc97557d32", "name": "chatId", "value": "={{ $('WA Webhook').item.json.body.data.chatId }}", "type": "string" }
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [1424, 544],
"id": "f021c824-6573-4ecb-a2ee-cc6c4c8cdd01",
"name": "Set Message and ID"
},
{
"parameters": {
"promptType": "define",
"text": "={{ $json.body.data.body }}",
"options": { "systemMessage": "You are a helpful assistant." }
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [1072, 512],
"id": "19742472-9a7b-4e65-8cff-a43a37065a4b",
"name": "Simple Agent"
}
],
"pinData": {},
"connections": {
"WA Webhook": {
"main": [[
{ "node": "Variety 1: Personal Contact", "type": "main", "index": 0 },
{ "node": "Variety 2: Group Message", "type": "main", "index": 0 },
{ "node": "Variety 3: @agent Mention", "type": "main", "index": 0 }
]]
},
"Ollama Chat Model": { "ai_languageModel": [[{ "node": "Simple Agent", "type": "ai_languageModel", "index": 0 }]] },
"Simple Memory": { "ai_memory": [[{ "node": "Simple Agent", "type": "ai_memory", "index": 0 }]] },
"Variety 3: @agent Mention": { "main": [[{ "node": "Simple Agent", "type": "main", "index": 0 }]] },
"Set Message and ID": { "main": [[{ "node": "Send WhatsApp Message", "type": "main", "index": 0 }]] },
"Simple Agent": { "main": [[{ "node": "Set Message and ID", "type": "main", "index": 0 }]] }
},
"active": false,
"settings": { "executionOrder": "v1" }
}
After importing: re-select credentials in Send WhatsApp Message and Ollama Chat Model. Change YourContactName in the Variety 1 IF node to the actual pushName from your webhook payload. Activate the workflow.
You're Done
At this point you have:
The stack is not the hard part. The hard part is the system prompt and deciding what to actually automate. Start simple — pick one variety, ship it, let people use it, fix what breaks.
Looking for the OpenWA API reference? → OpenWA API Reference for n8n Workflows — every endpoint with exact n8n HTTP Request node configuration.
Related Posts
Built something similar or want to talk through the architecture? Get in touch.