Mode d’emploi
- Copiez la commande et examinez le paquet avant de l’installer dans votre environnement agent.
- Consultez le document original pour les outils, identifiants et environnements requis.
- Décrivez les entrées et le livrable souhaité ; comparez la sortie aux sources.
npx skills add https://github.com/thatjuan/agent-skills --skill openwaAvant de commencer
- L’installation n’inclut ni abonnement au modèle, ni quota API, ni accès aux fichiers.
- Étoiles et installations reflètent l’activité, pas un test indépendant de qualité ou de sécurité.
Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
OpenWA
OpenWA is a self-hosted, open-source WhatsApp API Gateway (MIT, NestJS 11 / TypeScript / Node 22). It wraps whatsapp-web.js (Puppeteer + Chromium) behind a REST API on port 2785, ships a React dashboard on 2886, and supports multi-session WhatsApp automation with webhooks, Socket.IO real-time events, pluggable storage/cache/database, and a plugin hook system.
Product surface
| Component | Port | Role |
|---|---|---|
openwa-api (NestJS) | 2785 | REST + WebSocket + webhook dispatcher |
dashboard (React 19 + Vite) | 2886 | Session/API-key/webhook UI |
| Swagger UI | 2785 | /api/docs |
| Traefik (optional) | 80, 8080 | Reverse proxy + dashboard (profile with-proxy) |
| PostgreSQL (optional) | 5432 | Profile postgres |
| Redis (optional) | 6379 | Profile redis, gates BullMQ queue |
| MinIO (optional) | 9000, 9001 | Profile minio (S3-compatible storage) |
Global API prefix is /api (src/main.ts:128 app.setGlobalPrefix('api')). All paths below are relative to <baseUrl>/api.
Tech stack — verified
| Layer | Tech | Evidence |
|---|---|---|
| Framework | NestJS 11.x | package.json |
| WA engine | whatsapp-web.js 1.26.1 (Puppeteer + Chromium) | package.json, src/engine/engine.factory.ts |
| Main DB | SQLite (always — stores API keys, audit log) | src/app.module.ts |
| Data DB | SQLite or PostgreSQL (sessions, messages, webhooks) | src/app.module.ts |
| ORM | TypeORM 0.3.x | package.json |
| Queue | BullMQ 5.x (gated by QUEUE_ENABLED) | src/app.module.ts:31 |
| Cache | Redis (optional, gated by REDIS_ENABLED) | src/common/cache/ |
| Storage | Local or S3/MinIO | src/common/storage/ |
| Real-time | Socket.IO 4.8 on namespace /events | src/modules/events/events.gateway.ts |
| Validation | class-validator, class-transformer | package.json |
| SDK | @openwa/sdk (JS/TS) + openwa (Python) | sdk/javascript/, sdk/python/ |
Repository layout
src/
main.ts # Boot + .env precedence + first-run API key
app.module.ts # Composition; conditional QueueModule
config/configuration.ts # Env → config object
core/{hooks,plugins}/ # Plugin hook system
engine/ # IWhatsAppEngine interface + adapters
modules/
session/ # Session CRUD + in-memory engine map
message/ # Send & history endpoints
webhook/ # Per-session webhook subscriptions
events/ # Socket.IO gateway (/events namespace)
auth/ # API-key auth + roles
contact/ group/ label/ channel/ status/ catalog/
queue/ # BullMQ jobs (only if QUEUE_ENABLED)
health/ # /health, /health/live, /health/ready
infra/ settings/ stats/ audit/ docker/ plugins/
dashboard/ # React 19 + Vite SPA
sdk/{javascript,python}/ # Client libraries
traefik/{traefik.yml,dynamic.yml}
docker-compose.yml # Profiled production stack
docker-compose.dev.yml # SQLite + local dev quick-start
Dockerfile # Multi-stage Node 22-slim + Chromium
docs/ # 22 markdown design docsAuthentication — X-API-Key
Authentication is a hashed API key sent in the X-API-Key header (also accepted: Authorization: Bearer per main.ts:122 CORS allow-list). Keys are stored hashed in the main (SQLite) DB and have a role: ADMIN, OPERATOR, or VIEWER (src/modules/auth/entities/api-key.entity.ts).
curl -H "X-API-Key: owa_k1_xxxxxxxx..." http://localhost:2785/api/sessions- Key format:
owa_k1_<32 bytes hex>(src/modules/auth/auth.service.ts:95) - First-run bootstrap: if no API keys exist, OpenWA creates one and writes it to
data/.api-key, then prints it in the startup banner. In dev (NODE_ENV=development) the bootstrap key is the literal stringdev-admin-key. - Role gating: write endpoints carry
@RequireRole(ApiKeyRole.OPERATOR); reads are largely public-by-key. ADMIN is required for API-key CRUD. - Optional scoping per key: IP allow-list (
allowedIps) and session allow-list (allowedSessions).
See authentication & API-key lifecycle for key creation/revocation flows.
Sessions — multi-account WhatsApp
Each WhatsApp account = one persisted Session row + one in-memory engine (IWhatsAppEngine) keyed by session id (src/modules/session/session.service.ts). Sessions go through:
CREATED → INITIALIZING → QR_READY → AUTHENTICATING → READY
↘ FAILED
any → DISCONNECTED (also forced on every API restart)Minimum workflow to bring a session live:
# 1. Create
curl -X POST http://localhost:2785/api/sessions \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"bot-1"}'
# 2. Start (initialises Chromium + WA engine)
curl -X POST http://localhost:2785/api/sessions/$ID/start -H "X-API-Key: $KEY"
# 3. Fetch QR — returns SVG data while status = QR_READY
curl http://localhost:2785/api/sessions/$ID/qr -H "X-API-Key: $KEY"
# … scan with WhatsApp mobile …
# 4. Poll status (or subscribe via /events) until status = READY
curl http://localhost:2785/api/sessions/$ID -H "X-API-Key: $KEY"Chat ids follow WhatsApp Web conventions: <phone>@c.us for direct, <id>@g.us for groups, <id>@newsletter for channels. Invalid format = silent send failure.
Full lifecycle, in-memory engine map, proxy options, and reset-on-restart behaviour: operation.md.
REST API — controller catalog
All routes are mounted under /api. Roles in parentheses are required when stricter than the default VIEWER-or-higher.
| Domain | Routes | File |
|---|---|---|
| Sessions | POST /sessions (OPERATOR), GET /sessions, GET /sessions/:id, DELETE /sessions/:id (OPERATOR), POST /sessions/:id/start (OPERATOR), POST /sessions/:id/stop (OPERATOR), GET /sessions/:id/qr, GET /sessions/:id/groups, GET /sessions/stats/overview | session.controller.ts |
| Messages | GET /sessions/:sessionId/messages, POST /sessions/:sessionId/messages/send-text, …/send-image, …/send-video, …/send-audio, …/send-document, …/send-location, …/send-contact, …/send-sticker, …/reply, …/forward, …/react, GET …/:chatId/:messageId/reactions, POST …/delete, POST …/send-bulk, GET …/batch/:batchId, POST …/batch/:batchId/cancel | message.controller.ts |
| Webhooks | POST/GET/GET :id/PUT :id/DELETE :id under /sessions/:sessionId/webhooks, plus POST :id/test | webhook.controller.ts |
| API keys | POST /auth/api-keys (ADMIN), GET, GET :id, PUT :id, DELETE :id, POST :id/revoke | auth.controller.ts |
| Auth probe | GET /auth/validate (header X-API-Key) | auth-validate.controller.ts |
| Health | GET /health, GET /health/live, GET /health/ready (public; K8s-shaped) | health.controller.ts |
| Contact/Group/Label/Channel/Status/Catalog | Per-module controllers under /sessions/:sessionId/... | modules/<name>/ |
| Infra | Settings, stats, export/import for migration | modules/infra/ |
Authoritative shapes are the controller files plus the auto-published OpenAPI at http://localhost:2785/api/docs. Full route table with role/auth notes: rest-api.md.
Webhooks — per-session, signed, retried
Webhooks are created per session (POST /api/sessions/:sessionId/webhooks) with a URL, an events array, and optional secret/custom headers. Delivery is via BullMQ when QUEUE_ENABLED=true (with retries) and synchronous otherwise.
Headers emitted on every delivery:
| Header | Meaning |
|---|---|
X-OpenWA-Event | Event name (e.g. message.received) |
X-OpenWA-Delivery-Id | Unique per delivery attempt's parent dispatch |
X-OpenWA-Idempotency-Key | Stable per (event × subject) — dedupe key for consumers |
X-OpenWA-Retry-Count | 0 on first attempt, then attemptsMade |
X-OpenWA-Signature | sha256=<hex hmac> of the JSON body using webhook.secret (omitted if no secret) |
Defaults: WEBHOOK_TIMEOUT=10000 ms, WEBHOOK_MAX_RETRIES=3, WEBHOOK_RETRY_DELAY=5000 ms.
Subscribable event types (also valid on the WebSocket — src/modules/events/dto/ws-messages.dto.ts):
message.received, message.sent, message.ack, message.revoked, session.status, session.qr, session.authenticated, session.disconnected, group.join, group.leave, group.update — plus * for all.
Signature verification, delivery flow, queue vs sync, idempotency semantics, retry timing: webhooks-and-events.md.
Real-time — Socket.IO /events
Same event taxonomy as webhooks, delivered over Socket.IO on the /events namespace. The protocol is custom JSON (not the Socket.IO event emitter): clients send {type:'subscribe', sessionId, events, requestId?} and receive {type:'event', ...} / {type:'subscribed', ...} / {type:'error', ...} / {type:'pong'} server-side.
import { io } from 'socket.io-client';
const sock = io('http://localhost:2785/events', { auth: { apiKey: KEY } });
sock.emit('message', { type: 'subscribe', sessionId: '*', events: ['message.received'] });
sock.on('message', (msg) => { /* handle msg.type === 'event' */ });Wire format, auth modes, room layout (session:<id>:<event>), and ping/pong: webhooks-and-events.md#websocket.
Deployment
Three documented paths:
| Path | Command |
|---|---|
| Local dev (SQLite, no Redis/queue, all in one) | docker compose -f docker-compose.dev.yml up -d |
| Production basic | docker compose up -d |
| Production full stack (Postgres + Redis + MinIO + Traefik + Dashboard) | docker compose --profile full up -d |
The image is multi-stage node:22-slim with Chromium baked in and PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium. Container health check is node -e "require('http').get('http://localhost:2785/api/health', ...)" every 30 s.
Compose profiles (postgres, redis, minio, with-dashboard, with-proxy, full), volume layout (openwa-data:/app/data), Docker socket mount (/var/run/docker.sock:ro — used by modules/docker for orchestration), Traefik static/dynamic config, K8s health probes: deployment.md.
Configuration — env-driven, three-layer precedence
Bootstrapping (src/main.ts:11-65) loads dotenv with override: false so the highest priority wins, and it's process env:
process.env > ./.env > ./data/.env.generated.env.generated is the file the dashboard writes; it is the lowest priority, not the highest. A common pitfall is expecting dashboard saves to override Docker environment: blocks — they don't.
If no data/.env.generated exists on first boot, OpenWA writes one with SQLite, local storage, queue disabled, and prints a welcome banner with the API key.
Two TypeORM databases are wired in app.module.ts:
- `main` connection — always SQLite. Stores API keys, audit log. Stays node-local (don't try to migrate it).
- `data` connection — SQLite or PostgreSQL. Stores sessions, messages, webhooks. Synchronize defaults
truefor SQLite,falsefor Postgres (thennpm run migration:run:prod).
Full env-var reference, .env.minimal, dual-DB diagram, and QUEUE_ENABLED gating: configuration.md.
SDKs
import { OpenWAClient } from '@openwa/sdk';
const client = new OpenWAClient({ baseUrl: 'http://localhost:2785', apiKey: KEY });
await client.sessions.create({ name: 'bot-1' });
await client.sessions.start(id);
await client.messages.sendText(id, { chatId: '628123456789@c.us', text: 'hi' });The published @openwa/sdk ships a minimal hand-written surface (sdk/javascript/src/index.ts) and is intended to be regenerated from the OpenAPI spec — for endpoints it doesn't yet wrap, call client.request('POST', '/api/...', body) or use fetch with X-API-Key. A Python SDK lives at sdk/python/openwa/.
SDK install, typed wrappers, generation flow, and Python equivalents: sdk.md.
Architecture & extensibility
OpenWA's pluggability is enforced through interface boundaries, not feature flags alone:
- `IWhatsAppEngine` (
src/engine/interfaces/) — abstractswhatsapp-web.js; alternative engines (Baileys, etc.) can be dropped behind it viaengine.factory.ts. Selected byENGINE_TYPE. - `StorageService` — local-fs or S3/MinIO, swapped via
STORAGE_TYPE. - `CacheService` — in-memory or Redis, gated by
REDIS_ENABLED. - Plugin hooks —
core/hooks/runs named hook chains. The message-send pipeline firesmessage:sendingbefore dispatch (plugins can modify/block) andmessage:senton success. Plugins are auto-loaded whenPLUGINS_ENABLED=true. - Queue gating —
QueueModuleisrequired only whenQUEUE_ENABLED === 'true'(src/app.module.ts:31-38). Without it, webhook retries are best-effort sync only.
Module map, hook list, plugin loader, scaling model (engines pinned to nodes; API keys stay local; data DB can be shared): architecture.md.
Gotchas
A handful of behaviours that bite people who read the docs but skip the code:
.env.generatedis the lowest precedence, not the highest.- Every API restart force-resets all
READY/INITIALIZING/QR_READY/AUTHENTICATINGsessions toDISCONNECTED(engines are in-memory; restart loses them). QUEUE_ENABLED=false(the default) means webhooks have no retry — synchronous best-effort dispatch.synchronize: trueis the SQLite default and is dangerous in production; flip to migrations for Postgres.- API keys live in the SQLite
mainDB. Switching thedataDB to Postgres doesn't move them. phoneandpushNamearenulluntil the session reachesREADY.- Chat-id format must be
<phone>@c.us/<id>@g.us— wrong format = silent failure. - Chromium is mandatory; the Docker image hard-codes
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium.
Full annotated list with file:line references: gotchas.md.
Reference index
| Reference | What's inside |
|---|---|
| deployment.md | Dockerfile stages, compose profiles, Traefik, K8s probes, volumes |
| configuration.md | Env-var reference, precedence, dual-DB layout, queue gating |
| operation.md | Session lifecycle, QR auth, API-key bootstrap & lifecycle, dashboard |
| rest-api.md | Full controller→route catalog, role decorators, request/response shapes |
| webhooks-and-events.md | Webhook headers, HMAC, retries, idempotency, Socket.IO /events wire format |
| sdk.md | JS/TS and Python client usage, raw fetch fallback, regeneration notes |
| architecture.md | Module map, engine/storage/cache pluggability, plugin hooks, scaling |
| gotchas.md | Non-obvious behaviours verified against the source |



