Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

2,379 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

OpenWA Logo

OpenWA

Open Source WhatsApp API Gateway

FeaturesQuick StartDocsAPIContributing

CIVersionLicenseNodeNestJSDockerTypeScript


✨ 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 SourceNo licensing fees, no feature locks, full source code access
🏗️ Pluggable ArchitectureSwap adapters for database, storage, and cache via config
🖥️ Full DashboardModern React UI for session, webhook, and API key management
🔹 Multi-Session ReadyRun multiple WhatsApp sessions concurrently on one instance
🐳 Docker NativeProduction-ready with zero configuration
🧩 Official PluginsChatwoot, Typebot & more as sandboxed plugins on the Integration Fabric — OpenWA-plugins
🔗 n8n IntegrationCommunity nodes for workflow automation
🧩 Community AdaptersThird-party integrations (e.g. ioBroker) — see docs
🔐 Session-scoped keysOperator and viewer (reader) tokens can be limited to chosen sessions — or all sessions if none are selected

Session-scoped operator & viewer tokens

When you create or edit an operator or viewer API key in the dashboard, you can tick the WhatsApp sessions that key may use.

  • No sessions selected — the key can access every session, including ones created later.
  • One or more sessions selected — the key can only list, read, and (for operator) manage those sessions. A request naming any other session returns 401; session-filtered lists (sessions, audit, webhook delivery failures) return that key's rows rather than an error; and the key-management routes and the queue dashboard, which name no session at all, return 403.

Admin keys stay unscoped in the dashboard so they can keep managing other API keys. The HTTP API still accepts allowedSessions on any role if you need that from a client.


⚠️ 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:

    EngineBan-risk profileResource 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, use baileys.

Safe-sending guidelines

These are practical guardrails, not guarantees — but they materially reduce the chance of WhatsApp flagging the account:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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

FeatureStatusDescription
REST APIFull WhatsApp API via HTTP endpoints
Multi-SessionManage multiple WhatsApp accounts
WebhooksReal-time events with HMAC signature and optional smart pre-dispatch filters
Web DashboardVisual management interface
API Key AuthSecure API authentication
Swagger DocsInteractive API documentation

Messaging

FeatureStatusDescription
Text MessagesSend/receive text messages
Media MessagesImages, videos, documents, audio
Message ReactionsReact to messages with emoji
Message EditingSend edits + live message.edited events on both engines
Bulk MessagingSend to multiple recipients
Message StatusTrack delivery and read receipts

Advanced

FeatureStatusDescription
Groups APICreate, manage, join (invite code), and configure groups
Profile ManagementSet own display name, about text, and profile picture
Call Handlingcall.received events, reject calls, per-session auto-reject
Channels/NewsletterWhatsApp Channels support
Labels ManagementOrganize chats with labels
Proxy SupportPer-session proxy configuration
Rate LimitingConfigurable request limits
CIDR WhitelistingIP-based access control
Audit LoggingAudit 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)

Infrastructure

FeatureStatusDescription
SQLiteZero-config embedded database
PostgreSQLProduction-grade database
Redis CacheOptional performance caching
S3/MinIO StorageMedia-directory backup/migration backend
DockerOne-command deployment
Health ChecksKubernetes-ready probes
Data MigrationExport/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_HOST to be set:

systemctl --user start podman.socket
systemctl --user enable podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

Add the export line to your ~/.bashrc to make it permanent.

Option B: Local Development

# 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/docs

Use 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.


🔒 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, 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.

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 chown the named-volume mount points so the openwa user can write to them.
  • gosu performs a clean exec-based privilege drop — no su or sudo wrappers, 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
ProfileServices
postgresPostgreSQL database
redisRedis cache
minioS3-compatible storage
fullAll 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-api runs. 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 in docs/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 services

Official GHCR images are published as multi-arch manifests for:

  • linux/amd64
  • linux/arm64

🔌 Ports

ServicePortDescription
API & Dashboard2785REST API + bundled web dashboard (same port)
Swagger2785/api/docsInteractive API docs — off under NODE_ENV=production unless ENABLE_SWAGGER=true
Dashboard (dev)2886Vite 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 filters object 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 mounts 25 read-only tools by default — session, message, contact, group, webhook, label and automation-rule reads — because the surface is read-only unless you opt out. Add MCP_READONLY=false to mount all 51 tools, adding the write tier (send, reply, group operations). Either way it is a focused surface rather than the full API, so agents aren't overwhelmed.

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 (OPERATOR role 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=true to mount only the read tools (no sends/writes).
  • Set MCP_RATE_LIMIT_MAX (default 60) to limit tool calls per API key per window.
  • Set MCP_RATE_LIMIT_WINDOW_MS (default 60000) to control the sliding window size in milliseconds.
  • Do not expose /mcp to 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

LayerTechnology
RuntimeNode.js 22 LTS
FrameworkNestJS 11.x
LanguageTypeScript 6.x
WA Enginewhatsapp-web.js (default) / baileys — set ENGINE_TYPE
DatabaseSQLite / PostgreSQL
CacheRedis (optional)
StorageLocal / S3 / MinIO
ORMTypeORM
ContainerDocker + 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:

DocumentDescription
Project OverviewIntroduction and goals
RequirementsFeature specifications
ArchitectureSystem design
SecuritySecurity implementation
DatabaseData models and migrations
API SpecComplete API reference
DevelopmentCoding standards
Migration GuideDatabase & storage migration

🤝 Contributing

We welcome contributions! Here's how to get started:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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

Releases

Packages

Contributors

Languages