Free, Open Source, Self-Hosted WhatsApp API Gateway
npm install openwa
OpenWA
Open Source WhatsApp API Gateway
Features • Quick Start • Docs • API • Contributing
✨ Why OpenWA?
OpenWA is a free, open-source WhatsApp API Gateway designed for developers who need full control over their messaging infrastructure—without vendor lock-in or hidden paywalls.
Built on a pluggable architecture, OpenWA lets you select database engines (SQLite/PostgreSQL), backup/migration storage backends (Local/S3), and cache layers (disabled/Redis) through configuration rather than application-code changes. Message media itself is returned inline to API and webhook consumers; it is not automatically persisted to the storage backend.
| 🔓 100% Open Source | No licensing fees, no feature locks, full source code access |
| 🏗️ Pluggable Architecture | Swap adapters for database, storage, and cache via config |
| 🖥️ Full Dashboard | Modern React UI for session, webhook, and API key management |
| 🔹 Multi-Session Ready | Run multiple WhatsApp sessions concurrently on one instance |
| 🐳 Docker Native | Production-ready with zero configuration |
| 🧩 Official Plugins | Chatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins |
| 🔗 n8n Integration | Community nodes for workflow automation |
| 🧩 Community Adapters | Third-party integrations (e.g. ioBroker) — see docs |
⚠️ Before you connect a number — please read
OpenWA is an unofficial, community-maintained gateway. It connects to WhatsApp through reverse-engineered clients (the whatsapp-web.js project and @whiskeysockets/baileys), not through Meta's official Cloud API. This has real consequences you should understand before you link a phone number.
What this means in practice
There is always a non-zero risk of account restriction or ban. WhatsApp's anti-abuse systems actively look for unofficial automation. No amount of code quality on our side can make that risk zero.
Pick the right number. Never connect your primary personal or business number to an automated gateway. Use a dedicated number you can afford to lose. If you're running this for paying clients, pass that guidance on to them.
The two engines trade off differently:
Engine Ban-risk profile Resource cost whatsapp-web.jsLower — drives a real headless Chromium that looks like genuine WhatsApp Web traffic. High RAM (~300–500 MB / session). baileysHigher — speaks the multi-device WebSocket protocol directly and is easier for WhatsApp to fingerprint. Low RAM (~30–80 MB / session). If account safety is your top priority and you can afford the memory, prefer
whatsapp-web.js. If you need density and accept the trade-off, usebaileys.
Safe-sending guidelines
These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:
- Warm up fresh numbers. For the first several days, behave like a normal human user: scan the QR, exchange a handful of messages with saved contacts, join a group or two, set a profile photo. Don't blast on day one.
- Don't cold-blast strangers. Sending the first-ever message to a large batch of numbers that have never messaged you is the single most reliable way to get restricted — on either engine.
- Rate-limit yourself. OpenWA ships with a configurable rate limiter (
RATE_LIMIT_*env vars). Use it. A few messages per minute per session is sustainable; "thousands in an hour" is not. - Use opted-in recipients. The safest workloads are replies and alerts to people who already expect to hear from you (OTP to your own users, order updates, support replies).
- Keep a fallback. For anything auth-critical or revenue-critical, keep an SMS / email / official-Cloud-API path. Do not bet a login flow solely on an unofficial client.
- Mind the hosting IP. Cheap datacenter IPs are flagged more aggressively than residential ones. A residential proxy (supported per-session via the proxy settings) can help; it is not a license to spam.
Known platform behaviour (not bugs)
A few things that look like bugs but are actually server-side WhatsApp policy, not OpenWA defects — we track them separately so we can distinguish them from real bugs:
- First message to a brand-new contact sometimes never arrives. The API returns success because the message leaves OpenWA, but WhatsApp's server-side reach-out / trust policy drops it at delivery. This is independent of OpenWA. We track it in #830.
- Accounts that get restricted cannot be "unrestricted" by us. If WhatsApp disables a number, you need to appeal through their channels — OpenWA has no lever to pull.
Compliance
For any deployment where ethical, legal, or regulatory compliance matters (healthcare, finance, large-scale commercial messaging, anything touching end users in the EU/EEA under DMA/GDPR framings), treat OpenWA as not approved and use Meta's official WhatsApp Cloud API. OpenWA is an excellent fit for personal projects, internal tooling, automation hobbyists, and learning — it is not a drop-in replacement for the official API in regulated environments.
📖 For the deeper, maintainer-side risk analysis (protocol-change exposure, dependency strategy, security posture), see Risk Management (docs/16).
🎯 Features
Core Features
| Feature | Status | Description |
|---|---|---|
| REST API | ✅ | Full WhatsApp API via HTTP endpoints |
| Multi-Session | ✅ | Manage multiple WhatsApp accounts |
| Webhooks | ✅ | Real-time events with HMAC signature and optional smart pre-dispatch filters |
| Web Dashboard | ✅ | Visual management interface |
| API Key Auth | ✅ | Secure API authentication |
| Swagger Docs | ✅ | Interactive API documentation |
Messaging
| Feature | Status | Description |
|---|---|---|
| Text Messages | ✅ | Send/receive text messages |
| Media Messages | ✅ | Images, videos, documents, audio |
| Message Reactions | ✅ | React to messages with emoji |
| Message Editing | ✅ | Send edits + live message.edited events on both engines |
| Bulk Messaging | ✅ | Send to multiple recipients |
| Message Status | ✅ | Track delivery and read receipts |
Advanced
| Feature | Status | Description |
|---|---|---|
| Groups API | ✅ | Create, manage, join (invite code), and configure groups |
| Profile Management | ✅ | Set own display name, about text, and profile picture |
| Call Handling | ✅ | call.received events, reject calls, per-session auto-reject |
| Channels/Newsletter | ✅ | WhatsApp Channels support |
| Labels Management | ✅ | Organize chats with labels |
| Proxy Support | ✅ | Per-session proxy configuration |
| Rate Limiting | ✅ | Configurable request limits |
| CIDR Whitelisting | ✅ | IP-based access control |
| Audit Logging | ✅ | Track all API operations |
Infrastructure
| Feature | Status | Description |
|---|---|---|
| SQLite | ✅ | Zero-config embedded database |
| PostgreSQL | ✅ | Production-grade database |
| Redis Cache | ✅ | Optional performance caching |
| S3/MinIO Storage | ✅ | Media-directory backup/migration backend |
| Docker | ✅ | One-command deployment |
| Health Checks | ✅ | Kubernetes-ready probes |
| Data Migration | ✅ | Export/import between backends |
🚀 Quick Start
Option A: Docker (Recommended)
# Clone and start
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
docker compose -f docker-compose.dev.yml up -d
# Access (the dashboard is bundled into the API image and served on the same port)
# Dashboard: http://localhost:2785
# API: http://localhost:2785/api
# Swagger: http://localhost:2785/api/docs
Using Podman instead of Docker? Podman rootless mode requires the socket to be running and
DOCKER_HOSTto be set:systemctl --user start podman.socket systemctl --user enable podman.socket export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sockAdd the
exportline to your~/.bashrcto make it permanent.
Option B: Local Development
# Clone repository
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
# Install dependencies (includes dashboard)
npm install
# Start API + Dashboard (config is auto-generated on first run)
npm run dev
# Access (in dev the dashboard runs on the Vite server with hot reload)
# Dashboard: http://localhost:2886
# API: http://localhost:2785/api
# Swagger: http://localhost:2785/api/docs
🔒 Security Architecture
Docker Socket Proxy
The production stack never exposes /var/run/docker.sock directly to the application container. Instead, a dedicated docker-proxy sidecar (based on tecnativa/docker-socket-proxy) acts as the sole gateway to the Docker daemon:
openwa-api ──TCP 2375──▶ docker-proxy ──unix──▶ /var/run/docker.sock
Only the operations needed for container orchestration are enabled (CONTAINERS, IMAGES, VOLUMES, INFO, PING, POST, DELETE). The application connects via the DOCKER_HOST=tcp://docker-proxy:2375 environment variable, which DockerService detects automatically.
Non-root Container Execution
The production image never runs the Node.js process as root. On startup, the container follows this chain:
dumb-init (PID 1)
└─ docker-entrypoint.sh (root — fixes named-volume ownership via chown)
└─ gosu openwa node dist/main (drops to the openwa user)
- dumb-init is PID 1 and forwards signals (SIGTERM, etc.) for graceful shutdown.
- docker-entrypoint.sh runs as root only long enough to
chownthe named-volume mount points so theopenwauser can write to them. - gosu performs a clean
exec-based privilege drop — nosuorsudowrappers, so the node process is the direct child of dumb-init.
Named volumes (e.g. openwa-data) get their ownership corrected automatically on every start, so no manual chown step is needed after volume creation.
🏭 Production Deployment
For production, use the main docker-compose.yml with optional services:
# Basic production (SQLite, local storage)
docker compose up -d
# With PostgreSQL database
docker compose --profile postgres up -d
# Full stack (PostgreSQL, Redis, MinIO)
docker compose --profile full up -d
| Profile | Services |
|---|---|
postgres |
PostgreSQL database |
redis |
Redis cache |
minio |
S3-compatible storage |
full |
All services above |
The dashboard is bundled into the API image and served by NestJS on the API port, so it needs no profile — it is always available wherever
openwa-apiruns. For TLS/public exposure, put your own reverse proxy (nginx, Caddy, a cloud load balancer, or a k8s Ingress) in front; see the nginx example indocs/12-troubleshooting-faq.md.
Development vs Production
- Development (
docker-compose.dev.yml): SQLite, local storage, API serves the bundled dashboard- Production (
docker-compose.yml): Configurable database, profiles for optional servicesOfficial GHCR images are published as multi-arch manifests for:
linux/amd64linux/arm64
🔌 Ports
| Service | Port | Description |
|---|---|---|
| API & Dashboard | 2785 |
REST API + bundled web dashboard (same port) |
| Swagger | 2785/api/docs |
Interactive API docs |
| Dashboard (dev) | 2886 |
Vite dev server with hot reload (npm run dev) |
📡 API Examples
Create a Session
curl -X POST http://localhost:2785/api/sessions \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"name": "my-bot"}'
Start Session & Get QR Code
# Start the session
curl -X POST http://localhost:2785/api/sessions/{sessionId}/start \
-H "X-API-Key: YOUR_API_KEY"
# Get QR code (scan with WhatsApp)
curl http://localhost:2785/api/sessions/{sessionId}/qr \
-H "X-API-Key: YOUR_API_KEY"
Send a Message
curl -X POST http://localhost:2785/api/sessions/{sessionId}/messages/send-text \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"chatId": "628123456789@c.us",
"text": "Hello from OpenWA!"
}'
Setup Webhook
curl -X POST http://localhost:2785/api/sessions/{sessionId}/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"url": "https://your-server.com/webhook",
"events": ["message.received", "session.status"],
"secret": "your-hmac-secret"
}'
Smart filters (optional): add a
filtersobject to fire the webhook only when conditions match (AND), e.g.{ "conditions": [{ "field": "sender", "operator": "is", "value": ["1234567890@c.us"] }] }. Fields:sender/recipient/body/type/mentions/fromMe/hasMedia/isGroup. A webhook with no filters behaves exactly as before. See the API specification for the full schema.
🤖 MCP Server (AI Agents)
OpenWA can expose a curated set of tools over the Model Context Protocol so AI agents (Claude, Cursor, …) can drive WhatsApp. It is off by default and additive — every REST route keeps working unchanged.
Set MCP_ENABLED=true to mount a stateless Streamable-HTTP transport at POST /mcp on the existing server (same port, no extra process). It exposes ~39 curated tools (sessions, messaging, contacts, basic group ops, webhook reads) — a focused surface rather than the full API, so agents aren't overwhelmed and destructive operations stay off the agent path.
MCP_ENABLED=true npm run start:prod # or set MCP_ENABLED in your .env / compose
Point an MCP client at it (e.g. for Claude Code, a .mcp.json at your project root):
{
"mcpServers": {
"openwa": {
"type": "http",
"url": "http://localhost:2785/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
The key can be passed as Authorization: Bearer … or X-API-Key: …. Every tool call goes through the same API-key auth, role, and per-session scoping as REST.
Security guidance:
- Mint a dedicated, least-privilege key for the agent — a non-admin, session-scoped key (
OPERATORrole at most). The plaintext key is shown only once on creation; to rotate, create a new key and delete the old one. - The key must not carry an IP allow-list (
allowedIps) — there is no genuine client IP over MCP, so such a key is rejected. - Set
MCP_READONLY=trueto mount only the read tools (no sends/writes). - Set
MCP_RATE_LIMIT_MAX(default60) to limit tool calls per API key per window. - Set
MCP_RATE_LIMIT_WINDOW_MS(default60000) to control the sliding window size in milliseconds. - Do not expose
/mcpto the public internet without a fronting auth proxy. For a self-hosted, locally-reached deployment the static API key is appropriate; public exposure should use OAuth 2.1 (not yet built).
🛠 Tech Stack
| Layer | Technology |
|---|---|
| Runtime | Node.js 22 LTS |
| Framework | NestJS 11.x |
| Language | TypeScript 5.x |
| WA Engine | whatsapp-web.js (default) / baileys — set ENGINE_TYPE |
| Database | SQLite / PostgreSQL |
| Cache | Redis (optional) |
| Storage | Local / S3 / MinIO |
| ORM | TypeORM |
| Container | Docker + Docker Compose |
📁 Project Structure
openwa/
├── src/
│ ├── main.ts # Application entry point
│ ├── app.module.ts # Root module
│ ├── config/ # Configuration
│ ├── common/ # Shared utilities
│ │ ├── cache/ # Redis caching
│ │ └── storage/ # File storage (Local/S3)
│ ├── core/ # Core systems
│ │ ├── hooks/ # Plugin hooks
│ │ └── plugins/ # Plugin system
│ ├── engine/ # WhatsApp engine abstraction
│ └── modules/
│ ├── session/ # Session management
│ ├── message/ # Message handling
│ ├── webhook/ # Webhook management
│ ├── group/ # Groups API
│ ├── contact/ # Contacts API
│ ├── auth/ # API key authentication
│ ├── infra/ # Infrastructure management
│ └── health/ # Health checks
├── dashboard/ # React web dashboard
├── docs/ # Documentation
├── docker-compose.yml
├── Dockerfile
└── package.json
📚 Documentation
Comprehensive documentation is available in the docs/ folder:
| Document | Description |
|---|---|
| Project Overview | Introduction and goals |
| Requirements | Feature specifications |
| Architecture | System design |
| Security | Security implementation |
| Database | Data models and migrations |
| API Spec | Complete API reference |
| Development | Coding standards |
| Migration Guide | Database & storage migration |
🤝 Contributing
We welcome contributions! Here's how to get started:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please read our Development Guidelines for coding standards and best practices.
📄 License
This project is licensed under the MIT License – free for personal and commercial use.
See LICENSE for details.
OpenWA – Free, Open Source WhatsApp API Gateway
📖 Documentation · 🔌 API Docs · 🐛 Report Bug · 💡 Request Feature
Made with ❤️ by Yudhi Armyndharis and the OpenWA Community