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 capability namespaces, each gated by a permission it declared in its manifest.
The 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, chat history, canonical-id resolution) | Enriching events |
net:fetch | SSRF-guarded outbound HTTP, scoped to the manifest's net.allow host list (plus net.allowConfigHosts for operator-opened hosts) | Calling external APIs |
webhook:ingress | ctx.registerWebhook — claim an inbound route | Integration Fabric (receive external webhooks) |
conversation:send | ctx.conversations.send, ctx.handover, ctx.mappings | Integration Fabric (normalized send, handover, identity map) |
The first three are the base outbound surface every plugin can use. The last two are the Integration Fabric — for bidirectional external integrations (helpdesk/CRM/chatbot handover) running as sandboxed plugins without a separate server. See Integration Fabric.
A plugin that calls a capability it did not declare fails with a PluginCapabilityError. Capability calls are also confined to the sessions an operator activated the plugin for, not just the manifest's static scope. 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.7",
"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. A list of secret values (for example, a rotating set of API keys) also survives an append or removal: surviving entries keep their stored secret, while a genuinely new or edited row is never grafted with a stored value.
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.
On OpenWA ≥ 0.10.0 the dashboard intercepts an enable attempt when required config fields are unset and opens the plugin's config dialog with a warning, instead of the plugin landing in ERROR on a raw sandbox failure. Fields that declare a schema default are already seeded into the stored config at load, so only required fields without a default trigger the intercept.
Two runtime guarantees apply behind the scenes:
- Lifecycle ops are serialized per plugin id. Two concurrent
enablecalls for the same plugin can't double-runonEnableor register hooks twice; a racing caller is rejected. - Integration instance bindings self-heal on restart. A boot-time reconciler re-derives every enabled Integration Fabric instance's session binding from the persisted instance rows, so a binding lost while a plugin was momentarily unloaded is restored on the next boot without an operator re-PATCH. See Integration Fabric.
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. Override support is per-plugin — a plugin honors it only if it re-reads its config per event — and each official plugin's status is listed in the plugin catalog.
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.