WAHA — WhatsApp HTTP API

Self-hosted WhatsApp gateway for our infrastructure.
Source: waha.devlike.pro
Running on: IONOS VPS (vps-i1) — container openclaw-openclaw-gateway-1, ports 18789-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:

TierImageKey extras
Core (free)devlikeapro/wahaBasic messaging, no session persistence
Plus (paid)devlikeapro/waha-plusMedia 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:

EngineProtocolRAMMediaGroupsNotes
WEBJSBrowser (Chromium)~512 MBStable, resource-heavy
NOWEBNative (no browser)~50 MB✅ (Plus)Best for low-memory VPS
GOWSGo native~30 MBlimitedLightweight, less mature
WPPBrowser~512 MBSimilar to WEBJS
VENOMBrowser~512 MBDeprecated

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

TargetFormat
Individual48123456789@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 sendText

Poll (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

EventFires when
messageIncoming text, audio, or file
message.anyAny message created (including your own sends)
message.reactionEmoji reaction on a message
message.ackDelivery ack: PENDING / SERVER / DEVICE / READ / PLAYED
message.ack.groupGroup message acknowledgment
message.revokedMessage deleted by sender
message.editedMessage edited
message.waiting”Waiting for this message” status
session.statusSession state change (WORKING / FAILED / etc.)
group.v2.joinJoined or added to a group
group.v2.leaveLeft or removed from a group
group.v2.participantsSomeone joined/left a group
group.v2.updateGroup name/description changed
presence.updateTyping / online status
poll.voteUser voted in a poll
call.receivedIncoming call
call.acceptedCall accepted on another device
call.rejectedCall rejected
chat.archiveChat archived/unarchived
label.upsert / label.deletedLabel 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

  1. Never initiate cold outreach. Only message users who initiated contact first.
    • Drive inbound via https://wa.me/<number>?text=Hi
  2. No bulk messaging to unknown contacts.
  3. Never send 24/7 continuously — take natural breaks.
  4. Max ~4 messages per contact per hour for outbound.
  5. 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:18789 on the VPS.
  • Expose only the webhook endpoint (or MCP) via Caddy with TLS.
  • Do not bind WAHA to 0.0.0.0 or expose raw port to internet.

Scoped API keys (Plus)

Create per-app keys with minimal permissions:

AppRequired scopes
n8n workflow (send reports)send
Claude MCP (read + send)read, send
Monitoring (health checks)read
Admin operationsall 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=True

Endpoint

http://localhost:18789/mcp

Tools exposed to Claude

  • chats-get-messages — read chat history
  • chats-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 /mcp on wa.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

MethodEndpointPurpose
POST/api/sessionsCreate session
POST/api/sessions/{name}/startStart session
GET/api/sessions/{name}/auth/qrGet QR code image
GET/api/sessions/{name}Session status
POST/api/sendTextSend text
POST/api/sendImageSend image
POST/api/sendVoiceSend voice note
POST/api/sendFileSend file
POST/api/sendPollSend poll
POST/api/sendSeenMark messages as read
POST/api/startTypingStart typing indicator
POST/api/stopTypingStop typing indicator
POST/api/reactionSend emoji reaction
GET/api/messagesGet message history
GET/api/contactsList contacts
GET/api/groupsList groups
GET/api/Swagger UI
GET/dashboard/WAHA Dashboard
GET/mcpMCP server endpoint (Plus)

13. WAHA + n8n Integration

Source: waha.devlike.pro/blog/waha-n8n
n8n runs on Hostinger (vps-h1) at n8n.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

  1. Go to Credentials → New → WAHA API
  2. Set Host URL: http://217.154.82.162:18789 (internal) or https://wa.vps-i1.infra.zintegrowana.online
  3. Set API Key: value of WAHA_API_KEY
  4. 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

NodePurpose
WAHA TriggerStarts workflow when a WAHA event fires (incoming message, session status, etc.)
WAHA ActionsCalls 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:

CategoryExample actions
MessagessendText, sendImage, sendVoice, sendFile, sendPoll, sendSeen
ChatsgetChats, getMessages, deleteMessage
ContactsgetContact, getContacts, checkNumberExists
GroupsgetGroups, getGroup, createGroup, addParticipants
SessionsgetSessions, createSession, startSession, stopSession
ProfilegetProfile, setDisplayName, setProfilePicture
PresencesendPresenceOnline, startTyping, stopTyping

Troubleshooting

ProblemFix
WAHA Trigger not firingCheck webhook URL is set on the WAHA session; use Production URL not Test URL
save button missing in credentialsType any char in API Key field (n8n bug)
Media files 401Include X-Api-Key header in HTTP Request node when fetching /api/files/...
Bot replies to itselfAdd IF filter: {{ $json.payload.fromMe }} == false
Session drops after restartEnable 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