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 | β | Track all API operations |
| 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 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/docsThe 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.
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 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 |
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
