WAHA — WhatsApp HTTP API
Self-hosted WhatsApp gateway for our infrastructure.
Source: waha.devlike.pro
Running on: IONOS VPS (vps-i1) — containeropenclaw-openclaw-gateway-1, ports18789-18790
1. What Is WAHA
WAHA is a self-hosted REST API that exposes WhatsApp Web functionality over HTTP. It is not the official WhatsApp Business API — it wraps the unofficial WhatsApp Web protocol. This gives us full control but comes with blocking risk (see §6).
Two tiers:
| Tier | Image | Key extras |
|---|---|---|
| Core (free) | devlikeapro/waha | Basic messaging, no session persistence |
| Plus (paid) | devlikeapro/waha-plus | Media support, session persistence, multi-session, S3 storage, MCP server |
We need WAHA Plus to handle incoming/outgoing media (photos, documents, audio).
2. Engines
WAHA supports multiple underlying engines. Choose based on resource constraints and feature needs:
| Engine | Protocol | RAM | Media | Groups | Notes |
|---|---|---|---|---|---|
| WEBJS | Browser (Chromium) | ~512 MB | ✅ | ✅ | Stable, resource-heavy |
| NOWEB | Native (no browser) | ~50 MB | ✅ (Plus) | ✅ | Best for low-memory VPS |
| GOWS | Go native | ~30 MB | limited | ✅ | Lightweight, less mature |
| WPP | Browser | ~512 MB | ✅ | ✅ | Similar to WEBJS |
| VENOM | Browser | ~512 MB | ✅ | ✅ | Deprecated |
Our recommendation for IONOS VPS: NOWEB — low RAM footprint, supports media with Plus license.
Set engine via env var: WHATSAPP_DEFAULT_ENGINE=NOWEB
3. Our Deployment
IONOS VPS (vps-i1)
Container: openclaw-openclaw-gateway-1
Internal ports: 127.0.0.1:18789 (API), 127.0.0.1:18790 (additional)
Public URL: expose via Caddy → wa.vps-i1.infra.zintegrowana.online
Add to monitoring/Caddyfile:
wa.vps-i1.infra.zintegrowana.online {
reverse_proxy 127.0.0.1:18789
basicauth /* {
# Optional: add basic auth layer on top of X-Api-Key
}
}
Required env vars (add to monitoring/.env)
# WAHA Core
WAHA_API_KEY=<strong-random-uuid> # master API key
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=<strong-password>
WHATSAPP_DEFAULT_ENGINE=NOWEB
WAHA_APPS_ENABLED=True # enables MCP + other apps
# WAHA Plus license (after purchase)
WAHA_LICENSE_KEY=<license-key>
# Session persistence
WAHA_LOCAL_STORE_BASE_DIR=/app/.sessions
# Media storage (use local or Wasabi S3)
WHATSAPP_FILES_FOLDER=/app/.media
WHATSAPP_FILES_LIFETIME=0 # 0 = keep forever
# OR use Wasabi S3:
# WAHA_S3_BUCKET=ecotrans-waha-media
# WAHA_S3_REGION=eu-central-1
# WAHA_S3_ENDPOINT=s3.eu-central-1.wasabisys.com
# WAHA_S3_ACCESS_KEY_ID=<key>
# WAHA_S3_SECRET_ACCESS_KEY=<secret>4. Sessions
A session = one WhatsApp phone number connected to WAHA.
Session lifecycle states
STOPPED → STARTING → SCAN_QR_CODE → WORKING
↘ FAILED
Create & authenticate a session
# 1. Create session
curl -X POST http://localhost:18789/api/sessions \
-H "X-Api-Key: $WAHA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "default",
"config": {
"webhooks": [{
"url": "https://n8n.vps-h1.infra.zintegrowana.online/webhook/waha",
"events": ["message", "session.status", "message.reaction"]
}]
}
}'
# 2. Start session & get QR code
curl -X POST http://localhost:18789/api/sessions/default/start \
-H "X-Api-Key: $WAHA_API_KEY"
# 3. Get QR code (scan within 60s, then 20s per subsequent code, max 6)
curl http://localhost:18789/api/sessions/default/auth/qr \
-H "X-Api-Key: $WAHA_API_KEY"
# → returns PNG image or base64
# 4. Check status
curl http://localhost:18789/api/sessions/default \
-H "X-Api-Key: $WAHA_API_KEY"WAHA Plus: session persistence
With Plus, sessions survive container restarts — no re-scanning QR needed.
Add "start": true to session config for autostart.
5. Sending Messages — API Reference
All endpoints: POST http://localhost:18789/api/<endpoint>
Required headers: X-Api-Key: $WAHA_API_KEY, Content-Type: application/json
chatId format
| Target | Format |
|---|---|
| Individual | 48123456789@c.us |
| Group | <group-id>@g.us |
| Channel | <channel-id>@newsletter |
Text message
curl -X POST http://localhost:18789/api/sendText \
-H "X-Api-Key: $WAHA_API_KEY" \
-d '{
"session": "default",
"chatId": "48123456789@c.us",
"text": "Hello from WAHA!",
"linkPreview": true
}'Reply to a message
{
"session": "default",
"chatId": "48123456789@c.us",
"text": "Reply text",
"reply_to": "<message-id>"
}Image (POST /api/sendImage)
{
"session": "default",
"chatId": "48123456789@c.us",
"caption": "Fleet status screenshot",
"file": {
"url": "https://grafana.vps-i1.infra.zintegrowana.online/render/dashboard.png"
}
}Image must be JPEG. Convert other formats via ffmpeg before sending.
Voice message (POST /api/sendVoice)
{
"session": "default",
"chatId": "48123456789@c.us",
"file": { "url": "https://..." },
"convert": true
}Requires OPUS codec in OGG container. convert: true auto-converts.
File/document (POST /api/sendFile)
{
"session": "default",
"chatId": "48123456789@c.us",
"file": { "url": "https://..." }
}Mark as seen + typing simulation (anti-blocking pattern)
# 1. Send seen
curl -X POST http://localhost:18789/api/sendSeen \
-d '{"session":"default","chatId":"48123456789@c.us"}'
# 2. Start typing
curl -X POST http://localhost:18789/api/startTyping \
-d '{"session":"default","chatId":"48123456789@c.us"}'
# 3. Wait proportional to message length (~1s per 10 chars)
# 4. Stop typing + send
curl -X POST http://localhost:18789/api/stopTyping \
-d '{"session":"default","chatId":"48123456789@c.us"}'
# then sendTextPoll (POST /api/sendPoll)
{
"session": "default",
"chatId": "48123456789@c.us",
"poll": {
"name": "Which truck needs service?",
"options": ["WR 12345", "WR 67890", "WR 11111"],
"multipleAnswers": false
}
}6. Receiving Messages — Webhooks & Events
Webhook configuration (in session config)
{
"webhooks": [{
"url": "https://n8n.vps-h1.infra.zintegrowana.online/webhook/waha",
"events": ["message", "message.reaction", "session.status"],
"hmac": {
"key": "<hmac-secret>"
},
"retries": {
"policy": "exponential",
"maxRetries": 5,
"delaySeconds": 2
}
}]
}HMAC signature arrives in X-WAHA-Signature header — validate on n8n side.
Full events list
| Event | Fires when |
|---|---|
message | Incoming text, audio, or file |
message.any | Any message created (including your own sends) |
message.reaction | Emoji reaction on a message |
message.ack | Delivery ack: PENDING / SERVER / DEVICE / READ / PLAYED |
message.ack.group | Group message acknowledgment |
message.revoked | Message deleted by sender |
message.edited | Message edited |
message.waiting | ”Waiting for this message” status |
session.status | Session state change (WORKING / FAILED / etc.) |
group.v2.join | Joined or added to a group |
group.v2.leave | Left or removed from a group |
group.v2.participants | Someone joined/left a group |
group.v2.update | Group name/description changed |
presence.update | Typing / online status |
poll.vote | User voted in a poll |
call.received | Incoming call |
call.accepted | Call accepted on another device |
call.rejected | Call rejected |
chat.archive | Chat archived/unarchived |
label.upsert / label.deleted | Label management |
Incoming message payload (key fields)
{
"event": "message",
"session": "default",
"payload": {
"id": "msg-id",
"timestamp": 1234567890,
"from": "48123456789@c.us",
"fromMe": false,
"body": "Message text",
"hasMedia": false,
"type": "chat"
}
}When hasMedia: true, WAHA downloads the file and provides:
{
"mediaUrl": "http://localhost:18789/api/files/abc123.jpg"
}Access requires X-Api-Key header.
7. Anti-Blocking Rules
Source: waha.devlike.pro/docs/overview/how-to-avoid-blocking
WhatsApp flags accounts based on complaint accumulation. 5–10 spam reports → suspension risk.
Hard rules
- Never initiate cold outreach. Only message users who initiated contact first.
- Drive inbound via
https://wa.me/<number>?text=Hi
- Drive inbound via
- No bulk messaging to unknown contacts.
- Never send 24/7 continuously — take natural breaks.
- Max ~4 messages per contact per hour for outbound.
- Always send single, concise opening messages — no walls of text.
Humanization pattern (always apply for bot responses)
1. sendSeen ← mark their message as read
2. startTyping ← start typing indicator
3. wait (msg_length / 10 sec) ← simulate natural typing speed
4. stopTyping
5. sendText ← send the response
Message content rules
- Vary formatting between messages — never identical copies.
- Include personalization (name, context-specific details).
- Randomize delays between sends: 30–60 seconds minimum.
- Avoid URL shorteners that have been flagged; prefer direct HTTPS links.
- Never use previously-flagged domains.
- Segment contacts by area code for bulk operations.
Account health
- Maintain a complete WhatsApp profile: photo, display name, status.
- Encourage users to save the number — contacts in address book = lower risk.
- Conversations, group participation, and replies all increase account standing.
- Blocks and spam reports decrease account standing.
For our bots specifically
- The issue reporting group bot (DE number) only responds to incoming messages — this is the safest pattern.
- Grafana daily reports should go to a WhatsApp group (not individual DMs) and only once per day.
- Alert notifications should be throttled — batch into digests, not immediate fires per event.
8. Security
API authentication
# SHA512 hash (recommended for production)
WAHA_API_KEY=sha512:<hash-of-your-uuid>
# Plain text (dev only)
WAHA_API_KEY=<your-uuid>All API calls: X-Api-Key: <key> header.
For media URLs in <img> tags where headers aren’t possible: ?x-api-key=<key> query param.
Never put the API key in the Caddy public proxy without auth — use network isolation.
Dashboard & Swagger protection
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=<strong-password>
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=<strong-password>Network isolation
- WAHA should only be accessible via
127.0.0.1:18789on the VPS. - Expose only the webhook endpoint (or MCP) via Caddy with TLS.
- Do not bind WAHA to
0.0.0.0or expose raw port to internet.
Scoped API keys (Plus)
Create per-app keys with minimal permissions:
| App | Required scopes |
|---|---|
| n8n workflow (send reports) | send |
| Claude MCP (read + send) | read, send |
| Monitoring (health checks) | read |
| Admin operations | all scopes |
9. MCP Server
WAHA Plus exposes an MCP (Model Context Protocol) server, allowing Claude agents to interact with WhatsApp directly.
Enable
WAHA_APPS_ENABLED=TrueEndpoint
http://localhost:18789/mcp
Tools exposed to Claude
chats-get-messages— read chat historychats-delete-message— delete a message- Send text, image, file, voice messages
- Contact and group management
Connect Claude Code (local)
Since WAHA is on IONOS at 127.0.0.1:18789, access from local requires either:
- SSH tunnel:
ssh -L 18789:127.0.0.1:18789 root@217.154.82.162 - Caddy route: expose
/mcponwa.vps-i1.infra.zintegrowana.online/mcp
Add to .claude/settings.json in this repo:
"mcpServers": {
"waha": {
"type": "http",
"url": "https://wa.vps-i1.infra.zintegrowana.online/mcp",
"headers": {
"X-Api-Key": "${WAHA_API_KEY}"
}
}
}Set WAHA_API_KEY in your local environment or .env.local.
Connect from n8n (Hostinger VPS)
n8n runs on Hostinger and communicates with WAHA on IONOS.
Use the Caddy-proxied URL (not internal IP).
10. Use Cases for Our Infrastructure
10.1 WhatsApp Issue Reporting Group (active — issue #10)
Phone: DE number (OpenClaw/WAHA on IONOS)
Pattern: Reply-only bot — users send incident reports to the group, bot saves to p24_issues Supabase table.
Flow: WhatsApp message → WAHA webhook → n8n → Supabase insert
Risk: Low — reply-only, group context, single number.
10.2 Daily Fleet Report (planned)
Pattern: n8n cron at 07:00 → query Supabase → render Grafana panel as PNG → send via WAHA to manager group
WAHA endpoint: POST /api/sendImage with Grafana screenshot URL
Anti-blocking: Once per day, to an opt-in group → safe.
10.3 Critical Alert Escalation (planned)
Pattern: Alertmanager fires → n8n receives → batch digest → send to WhatsApp group
Throttle: Max 1 message per alert group per 15 minutes.
Do NOT: Send every Prometheus alert as individual WhatsApp message.
10.4 Driver Job Assignment Notifications (planned)
Pattern: Manager assigns job in ET platform → Supabase trigger → n8n → WAHA DM to driver
Anti-blocking: Driver opted in, single message per assignment, humanization pattern applied.
10.5 Claude Agent via MCP (experimental)
Pattern: Claude Code (local or on VPS) connects to WAHA MCP → can read conversations, send replies, manage sessions
Use cases: On-demand WhatsApp queries, manual message composition, session health checks
Scope: read + send only — never control or delete for AI agents.
11. Useful API Endpoints Quick Reference
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/sessions | Create session |
| POST | /api/sessions/{name}/start | Start session |
| GET | /api/sessions/{name}/auth/qr | Get QR code image |
| GET | /api/sessions/{name} | Session status |
| POST | /api/sendText | Send text |
| POST | /api/sendImage | Send image |
| POST | /api/sendVoice | Send voice note |
| POST | /api/sendFile | Send file |
| POST | /api/sendPoll | Send poll |
| POST | /api/sendSeen | Mark messages as read |
| POST | /api/startTyping | Start typing indicator |
| POST | /api/stopTyping | Stop typing indicator |
| POST | /api/reaction | Send emoji reaction |
| GET | /api/messages | Get message history |
| GET | /api/contacts | List contacts |
| GET | /api/groups | List groups |
| GET | /api/ | Swagger UI |
| GET | /dashboard/ | WAHA Dashboard |
| GET | /mcp | MCP server endpoint (Plus) |
13. WAHA + n8n Integration
Source: waha.devlike.pro/blog/waha-n8n
n8n runs on Hostinger (vps-h1) atn8n.vps-h1.infra.zintegrowana.online
Community node installation
In n8n UI: Settings → Community nodes → Install → search @devlikeapro/n8n-nodes-waha
WAHA API credentials in n8n
- Go to Credentials → New → WAHA API
- Set Host URL:
http://217.154.82.162:18789(internal) orhttps://wa.vps-i1.infra.zintegrowana.online - Set API Key: value of
WAHA_API_KEY - Known n8n bug: if Save button doesn’t appear, type any character in the API Key field first
For production: create a scoped API key in WAHA Dashboard (Settings → API Keys) with only read + send scopes, use that in n8n credentials — not the master key.
Two node types
| Node | Purpose |
|---|---|
| WAHA Trigger | Starts workflow when a WAHA event fires (incoming message, session status, etc.) |
| WAHA Actions | Calls any WAHA API endpoint (send text, image, get chats, etc.) |
Trigger setup — webhook wiring
The WAHA Trigger node generates a webhook URL. You must wire it into the WAHA session config:
In n8n: Add WAHA Trigger → copy the Production Webhook URL from the node settings.
In WAHA (curl or Dashboard):
curl -X PATCH http://localhost:18789/api/sessions/default \
-H "X-Api-Key: $WAHA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"config": {
"webhooks": [{
"url": "https://n8n.vps-h1.infra.zintegrowana.online/webhook/waha",
"events": ["message", "session.status", "message.reaction"]
}]
}
}'Or set the webhook URL at session creation time (see §4).
Core workflow patterns
Pattern 1 — Incoming message → reply
[WAHA Trigger: message]
→ [IF: payload.fromMe == false]
→ [WAHA Actions: sendSeen]
→ [WAHA Actions: startTyping]
→ [Wait: 2s]
→ [WAHA Actions: stopTyping]
→ [WAHA Actions: sendText]
chatId: {{ $json.payload.from }}
text: "Your reply here"
Key expression to get the sender: {{ $json.payload.from }}
Key expression to get message text: {{ $json.payload.body }}
Always filter fromMe == false — otherwise the bot replies to its own messages infinitely.
Pattern 2 — Session QR code → email
Automate QR delivery so you don’t need to watch the WAHA dashboard:
[WAHA Trigger: session.status]
→ [IF: payload.status == "SCAN_QR_CODE"]
→ [HTTP Request: GET /api/sessions/default/auth/qr?format=image]
Headers: X-Api-Key: $WAHA_API_KEY
→ [Gmail / SMTP: send email with QR as attachment]
Session status values to watch: SCAN_QR_CODE, WORKING, FAILED
Pattern 3 — Incoming message → AI response (Claude/OpenAI)
[WAHA Trigger: message]
→ [IF: fromMe == false AND NOT group message]
→ [sendSeen] → [startTyping]
→ [AI Agent node (Claude / OpenAI)]
system: "You are a fleet assistant for Ecotrans..."
input: {{ $json.payload.body }}
→ [Wait: proportional to response length]
→ [stopTyping]
→ [WAHA Actions: sendText]
chatId: {{ $json.payload.from }}
text: {{ $('AI Agent').item.json.text }}
Filter group messages: {{ !$json.payload.from.endsWith('@g.us') }}
Pattern 4 — Scheduled report → WhatsApp group
[Cron: 07:00 Mon-Fri]
→ [Supabase: query fleet KPIs]
→ [Grafana render: GET /render/d-solo/... → PNG]
→ [WAHA Actions: sendImage]
chatId: <group-id>@g.us
caption: "Fleet morning report {{ $now.toFormat('dd.MM.yyyy') }}"
file.url: (Grafana render URL)
Grafana render requires grafana-image-renderer container (already in our stack).
Grafana URL format: http://localhost:3000/render/d-solo/<dashboard-uid>/<panel-id>?...
Pattern 5 — Receive image → process → reply
[WAHA Trigger: message]
→ [IF: payload.hasMedia == true AND payload.type == "image"]
→ [HTTP Request: GET payload.mediaUrl]
Headers: X-Api-Key: $WAHA_API_KEY
→ [process / store / OCR / ...]
→ [WAHA Actions: sendText]
chatId: {{ $json.payload.from }}
text: "Received your image!"
Media URL from payload: {{ $json.payload.mediaUrl }}
Always include X-Api-Key header when fetching media files.
Pattern 6 — Bulk message sender (use with extreme care)
[Manual trigger / Webhook]
→ [Supabase: SELECT phone, message FROM campaigns WHERE sent = false]
→ [Split in Batches: batch size 1]
→ [Wait: random 30-60s] ← CRITICAL: must randomize
→ [WAHA Actions: sendText]
chatId: {{ $json.phone }}@c.us
text: {{ $json.message }}
→ [Supabase: UPDATE sent = true]
Anti-blocking for bulk: Never skip the random wait. Max 4/hour per contact. Only message opted-in contacts.
Available WAHA Action categories in n8n
The WAHA Actions node exposes the full REST API grouped by category:
| Category | Example actions |
|---|---|
| Messages | sendText, sendImage, sendVoice, sendFile, sendPoll, sendSeen |
| Chats | getChats, getMessages, deleteMessage |
| Contacts | getContact, getContacts, checkNumberExists |
| Groups | getGroups, getGroup, createGroup, addParticipants |
| Sessions | getSessions, createSession, startSession, stopSession |
| Profile | getProfile, setDisplayName, setProfilePicture |
| Presence | sendPresenceOnline, startTyping, stopTyping |
Troubleshooting
| Problem | Fix |
|---|---|
| WAHA Trigger not firing | Check webhook URL is set on the WAHA session; use Production URL not Test URL |
save button missing in credentials | Type any char in API Key field (n8n bug) |
| Media files 401 | Include X-Api-Key header in HTTP Request node when fetching /api/files/... |
| Bot replies to itself | Add IF filter: {{ $json.payload.fromMe }} == false |
| Session drops after restart | Enable WAHA Plus session persistence + autostart: true in session config |
12. Docker Compose Snippet
waha:
image: devlikeapro/waha-plus
container_name: openclaw-openclaw-gateway-1
restart: unless-stopped
ports:
- "127.0.0.1:18789:3000"
volumes:
- ./.sessions:/app/.sessions
- ./.media:/app/.media
environment:
- WAHA_API_KEY=${WAHA_API_KEY}
- WAHA_DASHBOARD_USERNAME=${WAHA_DASHBOARD_USERNAME}
- WAHA_DASHBOARD_PASSWORD=${WAHA_DASHBOARD_PASSWORD}
- WHATSAPP_DEFAULT_ENGINE=NOWEB
- WAHA_APPS_ENABLED=True
- WAHA_LICENSE_KEY=${WAHA_LICENSE_KEY}
- WHATSAPP_FILES_FOLDER=/app/.media
- WHATSAPP_FILES_LIFETIME=0