Open Source WhatsApp API Gateway
Features • Quick Start • Docs • API • Contributing
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 |
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.
-
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.
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.
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.
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).
| 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 |
| 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 |
| 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 | ✅ | Audit trail for API-key, session, integration-instance, and infra admin operations (message sends and webhook deliveries are tracked in their own tables, not the audit log) |
| 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 |
# 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/docsUsing 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.
# Clone repository
git clone https://github.com/rmyndharis/OpenWA.git
cd OpenWA
# Install the locked dependencies (includes dashboard)
npm ci
# 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/docsUse npm install instead when intentionally changing dependencies. OpenWA's committed lockfile uses
registry artifacts only, so npm 12 works with its secure default that blocks Git dependencies; do not
disable that policy globally.
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, plus the POST method switch). The application connects via the DOCKER_HOST=tcp://docker-proxy:2375 environment variable, which DockerService detects automatically. Note this is an operational gateway, not a fine-grained privilege boundary: with POST enabled the proxy admits every method to the enabled paths and cannot scope container-create payloads, so a compromised API container would be host-root-equivalent — see SECURITY.md for the full threat model, mitigations, and how to disable the proxy if you don't use the built-in datastore orchestration.
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.
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
| 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) |
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 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"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!"
}'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.
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 / composePoint 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).
| Layer | Technology |
|---|---|
| Runtime | Node.js 22 LTS |
| Framework | NestJS 11.x |
| Language | TypeScript 6.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 |
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
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 |
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.
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
