Install and enable a plugin
Plugins extend a running OpenWA gateway in-process: they react to WhatsApp activity through typed lifecycle hooks and act through a narrow, permission-gated capability API — no fork, no core changes. This page shows you how to install a plugin, configure it, and enable it on a live gateway, then points you to the rest of the section.
A plugin is a self-contained folder — a manifest.json plus a compiled entry file — packaged as a .zip. It is installed disabled and runs only after an administrator explicitly enables it. Management is admin-only, over the REST API or the dashboard's Plugins page.
- A running OpenWA gateway. See Installation.
- An ADMIN-role API key (every plugin route requires it). See Authentication.
- A plugin
.zip. Grab one from the official catalog, or build one (see Building a plugin).
How plugins fit together
A plugin never touches the gateway's internals. It registers handlers for hook events and calls back into the host through three capability namespaces, each gated by a permission it declared in its manifest.
The three permissions a plugin can declare:
| Permission | Grants | Used for |
|---|---|---|
messages:send | ctx.messages.sendText / ctx.messages.reply | Auto-replies, notifications |
engine:read | Read-only engine queries (group info, contacts, chats, number check) | Enriching events |
net:fetch | SSRF-guarded outbound HTTP, scoped to the manifest's net.allow host list | Calling external APIs |
A plugin that calls a capability it did not declare fails with a PluginCapabilityError. For the full hook list, capability surface, and lifecycle, see Plugin architecture.
Disk-loaded plugins run in a worker_thread (V8-context isolation), but that is not an OS-level sandbox — a plugin can reach Node built-ins like fs and process. Install only plugins you trust, and review a plugin's declared permissions and sessions before enabling it.
The install flow
Installing a plugin from a .zip is a three-step sequence — install, configure, enable — because OpenWA never auto-runs freshly uploaded code.
All routes sit under the /api prefix and require an ADMIN key in the X-API-Key header. The examples below use the faq-bot plugin, which auto-replies from configurable keyword rules.
1. Install the package
Upload the .zip as multipart form field file:
curl -X POST "http://localhost:2785/api/plugins/install" \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@faq-bot.zip"
The plugin is now installed but disabled — it is not running yet:
{
"id": "faq-bot",
"name": "FAQ / Auto-Reply Bot",
"version": "0.1.4",
"type": "extension",
"status": "installed",
"builtIn": false
}
If your gateway is configured with the official catalog, you can skip the build-and-upload step and install by URL instead: POST /api/plugins/install-url with body { "url": "<release-zip-url>" }. The download is SSRF-guarded and restricted to an allowlisted host. See the catalog.
2. Configure it
Each plugin declares a configSchema in its manifest. Send the settings as a config object. For faq-bot, that means the reply rules and an optional fallback:
curl -X PUT "http://localhost:2785/api/plugins/faq-bot/config" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"config": {
"rules": "[{\"mode\":\"contains\",\"pattern\":\"hours\",\"reply\":\"We are open 9-5, Mon-Fri.\"}]",
"fallbackReply": "Thanks! Someone will reply shortly.",
"respondInGroups": false
}
}'
{ "id": "faq-bot", "status": "installed", "config": { "rules": "[...]", "respondInGroups": false } }
Config fields marked secret: true (an API key, a service-account JSON) are masked on read and preserved on an unchanged write — so you never have to re-paste a secret to edit an unrelated field.
OpenWA renders configSchema into the dashboard form and redacts secret fields, but it does not enforce the schema's types on write. A well-built plugin validates its own config defensively when it starts.
3. Enable it
Enabling runs the plugin's onLoad then onEnable lifecycle and registers its hooks:
curl -X POST "http://localhost:2785/api/plugins/faq-bot/enable" \
-H "X-API-Key: YOUR_API_KEY"
{ "id": "faq-bot", "status": "enabled", "enabledAt": "2026-06-26T10:15:00.000Z" }
The plugin is now live. Send a WhatsApp message containing "hours" to a connected session and it auto-replies. Confirm the running state any time:
curl "http://localhost:2785/api/plugins/faq-bot" \
-H "X-API-Key: YOUR_API_KEY"
A plugin never auto-enables — not after install, and not after a gateway restart. A previously enabled plugin comes back as installed on restart and must be re-enabled. This keeps untrusted code from running silently.
Scope a plugin to specific sessions
By default a plugin that declares sessions: ["*"] acts on every session. To restrict a session-scoped plugin to a subset, set its active sessions:
curl -X PUT "http://localhost:2785/api/plugins/faq-bot/sessions" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sessions": ["sales", "support"] }'
Use ["*"] for all sessions or [] for none. The session scope a plugin may act on is fixed by its manifest — activation can narrow it but never widen it past what the manifest's sessions allows.
To give one session different settings, set a per-session config override with PUT /api/plugins/{id}/config/{sessionId} (send an empty config to clear it). The override is shallow-merged over the base config for that session's events.
Management endpoints
Every route below sits under /api and requires the ADMIN role.
| Method & path | Purpose |
|---|---|
GET /api/plugins | List installed plugins and their status |
GET /api/plugins/catalog | List the remote catalog, annotated with install state |
GET /api/plugins/{id} | Inspect one plugin (config secrets redacted) |
POST /api/plugins/install | Install from an uploaded .zip (multipart field file) |
POST /api/plugins/install-url | Install by downloading a .zip from an allowlisted URL |
POST /api/plugins/{id}/update | Update in place from a URL (keeps config + enabled state) |
POST /api/plugins/{id}/enable | Enable (runs onLoad → onEnable) |
POST /api/plugins/{id}/disable | Disable and unregister its hooks |
PUT /api/plugins/{id}/config | Update base config (body { "config": { … } }) |
PUT /api/plugins/{id}/config/{sessionId} | Set or clear a per-session config override |
PUT /api/plugins/{id}/sessions | Set which sessions a scoped plugin is active for |
GET /api/plugins/{id}/health | Run the plugin's healthCheck |
DELETE /api/plugins/{id} | Uninstall and delete files (built-ins are protected) |
A plugin's status is one of installed, enabled, disabled, or error. The full request and response shapes are in the API reference.
Disable or uninstall
Disable a plugin to stop it without losing its config:
curl -X POST "http://localhost:2785/api/plugins/faq-bot/disable" \
-H "X-API-Key: YOUR_API_KEY"
Uninstall to remove it and its files entirely:
curl -X DELETE "http://localhost:2785/api/plugins/faq-bot" \
-H "X-API-Key: YOUR_API_KEY"
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized | Key missing or not ADMIN-scoped | Use an ADMIN-role key in X-API-Key. See Authentication. |
404 Not Found on a plugin route | Plugin id not installed | Confirm the id with GET /api/plugins; ids are case-sensitive. |
Plugin shows installed after a restart | Plugins never auto-enable | Re-enable it with POST /api/plugins/{id}/enable. |
Plugin is enabled but does nothing | Empty/invalid config, or wrong session scope | Re-check the config object and the plugin's active sessions. |
error status after enable | Plugin threw during onLoad/onEnable | Inspect the error field from GET /api/plugins/{id} and the gateway logs. |
Capability call fails with PluginCapabilityError | Plugin used a permission it did not declare, or an out-of-scope session | This is the plugin author's fix — see Building a plugin. |
Next steps
- Browse ready-to-install plugins in the plugin catalog.
- Understand hooks, capabilities, and the sandbox in Plugin architecture.
- Write your own with Building a plugin.
- Ship it to others via Publishing a plugin.