# OpenWA Plugins **Official & community plugins for [OpenWA](https://github.com/rmyndharis/OpenWA) — the open-source WhatsApp API Gateway.** Extend your WhatsApp gateway with drop-in capabilities: log conversations to a spreadsheet, auto-reply to customers, greet new leads, and more — installed in seconds, no fork required. [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![Built for OpenWA](https://img.shields.io/badge/built%20for-OpenWA-25D366.svg)](https://github.com/rmyndharis/OpenWA) [![Plugin type](https://img.shields.io/badge/plugin%20type-extension-blue.svg)](#plugin-contract) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing) [![Buy Me a Coffee](https://img.shields.io/badge/Buy_Me_a_Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/rmyndharis) [Install a plugin](#installing-a-plugin) · [Plugin catalog](#plugin-catalog) · [Write your own](#authoring-a-plugin) · [Contributing](#contributing)
--- > **Status:** Active — a growing catalog. This repository is the home for installable OpenWA plugins and the conventions for building them. The catalog grows as plugins reach release. ## Overview OpenWA ships with a small, security-first plugin runtime. A plugin is a self-contained folder — a `manifest.json` plus a compiled entry file — that reacts to WhatsApp activity through a typed **hook** system and acts through a narrow, permission-gated **capability** API. Plugins are uploaded as a `.zip`, loaded in a **disabled** state, and only run after an administrator explicitly enables them. This repository provides: - **A curated catalog** of ready-to-install plugins, each packaged for the OpenWA dashboard. - **A reference authoring workflow** — vendored OpenWA types, a one-command build-and-package script, and a worked example you can copy. - **Accurate, code-verified documentation** of the plugin contract (the upstream design docs describe several features that are not yet implemented; everything here reflects the shipped runtime). ## Plugin catalog | Plugin | Description | Version | Status | | ------ | ----------- | ------- | ------ | | [`after-hours`](./after-hours) | Auto-replies with a configurable away/closing message to messages received outside business hours. | 0.2.15 | stable | | [`ai-responder`](./ai-responder) | Auto-replies to inbound WhatsApp messages with an OpenAI-compatible Chat Completions API. Single-turn, rate-limited per chat and per session, and last in the responder chain. | 0.1.6 | beta | | [`chat-flow`](./chat-flow) | Interactive, stateful auto-reply: a trigger word starts a greeting + numbered menu, replies traverse a configurable menu tree, and per-chat state expires after 15 minutes. | 1.1.18 | stable | | [`chatwoot-adapter`](./chatwoot-adapter) | Two-way sync between a WhatsApp session and a Chatwoot inbox: relays WhatsApp messages (1:1 and groups, with media) into Chatwoot as an API-channel inbox, sends agent replies back to WhatsApp, and hands a chat over to a human agent — silencing other OpenWA bots — when an agent takes it in Chatwoot. First consumer of the OpenWA Integration SDK v1; runs sandboxed in the plugin worker. | 0.9.17 | stable | | [`faq-bot`](./faq-bot) | Auto-replies to inbound WhatsApp messages from configurable FAQ keyword/regex rules. | 0.2.20 | stable | | [`group-translate`](./group-translate) | Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled. | 1.3.17 | stable | | [`gsheets-logger`](./gsheets-logger) | Logs WhatsApp message events to a Google Sheet via a service account. | 0.3.18 | stable | | [`http-action`](./http-action) | Triggers safe REST API requests from WhatsApp commands and renders JSON responses back to chat. | 0.2.18 | stable | | [`supabase-otp-hook`](./supabase-otp-hook) | Deliver Supabase Auth phone OTPs over WhatsApp. | 0.3.16 | beta | | [`typebot-connector`](./typebot-connector) | Runs a Typebot flow as the brain of a WhatsApp bot: inbound messages drive a Typebot chat session via the live Chat API, and the bot's replies — text, media, and numbered-choice inputs — are sent back to WhatsApp. Auto-starts every chat, handles file-upload steps, and resets when the flow ends or after an idle timeout. Runs sandboxed in the plugin worker; no public URL or webhook required. | 0.3.9 | stable | | [`voice-transcription`](./voice-transcription) | Transcribes inbound WhatsApp voice notes to text via an OpenAI-compatible speech-to-text backend (self-hosted Speaches/faster-whisper or hosted Groq/OpenAI) and delivers a `message.transcription` event to your webhook — so bots and AI can read and reply to audio. Off the message-delivery path; disabled until enabled. | 1.3.7 | beta | The table above is generated from each plugin's `manifest.json` + `CHANGELOG.md` by `npm run catalog` (and mirrored in [`plugins.json`](./plugins.json)). See [PLUGIN-STANDARD.md](./PLUGIN-STANDARD.md) for the metadata standard every plugin follows. ## Per-session config support OpenWA's `sessionScoped` plugins (the default) may carry per-session config **overrides** set via the dashboard or `PUT /api/plugins/:id/config/:sessionId` — so two WhatsApp sessions under one plugin instance can run different settings. A plugin honors overrides only if it re-reads `ctx.config` inside its hook (not a cached snapshot from enable). The table below is the status for each plugin in this repo. See each plugin's README **Compatibility → Per-session config** for details and caveats. | Plugin | Per-session config | Notes | | ------ | :----------------: | ----- | | [`after-hours`](./after-hours) | ✅ Supported | All fields per session; takes effect on next message. | | [`ai-responder`](./ai-responder) | ✅ Supported | All fields per session (different providers, models and prompts); rate limits are in memory and reset on restart. | | [`chat-flow`](./chat-flow) | ✅ Supported | All fields per session; flow state is per `(session, chat)`. | | [`chatwoot-adapter`](./chatwoot-adapter) | ⚠️ Supported, with caveat | All fields per session, the first-class multi-tenant shape; the failed-relay retry queue drains only once that session dispatches an event again. | | [`faq-bot`](./faq-bot) | ✅ Supported | All fields per session (different rule sets per number). | | [`group-translate`](./group-translate) | ⚠️ Supported, with caveat | Config-signature caching; multi-backend isolation needs one instance per session. | | [`gsheets-logger`](./gsheets-logger) | ❌ Not supported | Single-buffer single-sink design; use one instance per session. | | [`http-action`](./http-action) | ✅ Supported | All fields per session (different endpoints/action sets). | | [`supabase-otp-hook`](./supabase-otp-hook) | ✅ Supported | All fields per session; applies to the instance's bound session. | | [`typebot-connector`](./typebot-connector) | ✅ Supported | All fields per session; flow state is per `(session, chat)`. | | [`voice-transcription`](./voice-transcription) | ⚠️ Supported, with caveat | Config-signature caching; multi-backend isolation needs one instance per session. | **On the roadmap:** an automatic closing-greeting plugin for new leads. Want something else? [Open an issue](https://github.com/rmyndharis/OpenWA-plugins/issues) or [contribute one](#contributing). ## Installing a plugin Plugins are managed by an **ADMIN** API key, either through the OpenWA dashboard (**Plugins** section) or directly over the REST API. Authenticate with either the `X-API-Key` header or `Authorization: Bearer `. > Replace `https://your-openwa-host` with your gateway's base URL and `` with an admin-scoped key. **1. Build the plugin package** (see [Building from source](#building-from-source)) to get `gsheets-logger.zip`, then upload it: ```bash curl -X POST "https://your-openwa-host/api/plugins/install" \ -H "X-API-Key: " \ -F "file=@gsheets-logger.zip" ``` Installing from a URL instead of an upload works the same way (`POST /api/plugins/install-url` with `{"url": ...}`). On a production host (OpenWA ≥ 0.20.0, where `NODE_ENV=production`), a URL install additionally requires a `#sha256=<64 hex>` integrity pin on the URL: catalog entries already carry it, and for a hand-pasted release URL the digest is the SHA-256 printed in that plugin's GitHub Release notes (`PLUGIN_INSTALL_REQUIRE_PIN=false` opts out). **2. Configure it** (secrets are masked on read and preserved on write; from OpenWA 0.26.0 sending `null` for one removes it): ```bash curl -X PUT "https://your-openwa-host/api/plugins/gsheets-logger/config" \ -H "X-API-Key: " \ -H "Content-Type: application/json" \ -d '{ "config": { "spreadsheetId": "1AbC...defG", "serviceAccountJson": "{...}", "sheetTab": "Logs" } }' ``` **3. Enable it** — a freshly installed plugin is disabled and never enables itself. Once you have enabled it, that decision is remembered: OpenWA ≥ 0.10.5 re-enables it automatically on every host restart. An ERROR status does not exclude it — the host replays your enable decision, not the last runtime status. What is not automatic is a worker that crashed mid-run: nothing respawns it until the next restart or an explicit re-enable. ```bash curl -X POST "https://your-openwa-host/api/plugins/gsheets-logger/enable" \ -H "X-API-Key: " ``` ### Management endpoints All routes require an ADMIN role. Every one of them also rejects a session-scoped API key outright, except `PUT /api/plugins/:id/config/:sessionId`, which a scoped key may call. | Method & path | Purpose | | ------------- | ------- | | `GET /api/plugins` | List installed plugins and their status | | `GET /api/plugins/catalog` | The remote catalog, annotated with what is already installed | | `GET /api/plugins/:id` | Inspect one plugin (config secrets redacted) | | `POST /api/plugins/install` | Upload and install a `.zip` (multipart field `file`) | | `POST /api/plugins/install-url` | Install by downloading a `.zip` from a URL (SSRF-guarded, `#sha256=` pinned) | | `POST /api/plugins/:id/enable` | Run the plugin (`onLoad` → `onEnable`) | | `POST /api/plugins/:id/disable` | Stop the plugin and unregister its hooks | | `POST /api/plugins/:id/update` | Replace an installed plugin in place from a URL, keeping its config and enabled state | | `PUT /api/plugins/:id/config` | Update the base config (`{ "config": { ... } }`); fires `onConfigChange` if enabled | | `PUT /api/plugins/:id/config/:sessionId` | Set one session's config override, shallow-merged over the base; an empty object clears it | | `PUT /api/plugins/:id/sessions` | Replace the whole set of sessions a session-scoped plugin is active for | | `GET /api/plugins/:id/config-ui` | The plugin's sandboxed-iframe config editor, when it ships one | | `DELETE /api/plugins/:id` | Uninstall and remove files (built-ins are protected) | | `GET /api/plugins/:id/health` | Plugin-reported health check | The gateway mounts every route under a global `/api` prefix, so the paths above are the complete ones — dropping it answers 404. ## Plugin contract Everything below is verified against the OpenWA runtime, not the aspirational design docs. A plugin is a directory containing: ``` my-plugin/ ├─ manifest.json # metadata, declared hooks, permissions, config schema └─ dist/index.js # compiled entry; default-exports a class implementing IPlugin ``` ### Manifest ```jsonc { "id": "my-plugin", // /^[a-z0-9][a-z0-9._-]*$/i, unique, not reserved "name": "My Plugin", "version": "1.0.0", // semver "type": "extension", // only "extension" is user-installable "main": "dist/index.js", // require()-able file inside the package "description": "…", "permissions": ["messages:send"], // enforced gates: messages:send · engine:read · net:fetch · webhook:ingress · conversation:send · storage:use · search:provide "sessions": ["*"], // session-id scope; omit ⇒ all sessions "hooks": ["message:received"], // declared interest (informational) "configSchema": { // drives the dashboard settings form "type": "object", "properties": { "apiKey": { "type": "string", "secret": true, "required": true } } } } ``` **Required fields:** `id`, `name`, `version`, `type`, `main`. **Reserved ids** (cannot be shadowed): `whatsapp-web.js`, `baileys`, `auto-reply`, `translation`. ### Entry class — the `IPlugin` lifecycle The entry file must **`export default` a class** with a no-argument constructor. Every lifecycle method is optional; the loader instantiates the class and calls `onLoad` then `onEnable` when an admin enables the plugin. ```ts import type { IPlugin, PluginContext } from "../types/openwa"; export default class MyPlugin implements IPlugin { async onEnable(ctx: PluginContext): Promise { ctx.registerHook("message:received", async (hook) => { ctx.logger.log(`Inbound message on session ${hook.sessionId}`); return { continue: true }; // never blocks normal processing }); } async onDisable(_ctx: PluginContext): Promise { // release timers/connections; hooks are auto-unregistered for you } async healthCheck() { return { healthy: true }; } } ``` | Method | When it runs | | ------ | ------------ | | `onLoad(ctx)` | At enable, before `onEnable` — one-time setup | | `onEnable(ctx)` | When enabled — register hooks and start work here | | `onDisable(ctx)` | When disabled — tear down; hooks auto-unregister | | `onUnload(ctx)` | Before removal from memory | | `onConfigChange(ctx, newConfig)` | When config is updated (only while enabled) | | `healthCheck()` | On `GET /api/plugins/:id/health` | ### Hooks React to activity with `ctx.registerHook(event, handler, priority?)`. Handlers are `async (ctx: HookContext) => Promise>`, run in **ascending priority** order (lower runs first; default `100`), and receive `{ event, data, sessionId?, timestamp, source }`. Return `{ continue: true }` to pass through, `{ continue: false }` to stop the chain, or `{ continue: true, data }` to transform the payload for downstream handlers. | Group | Events | | ----- | ------ | | **Session** | `session:created` · `session:starting` · `session:ready` · `session:qr` · `session:disconnected` · `session:error` · `session:deleted` | | **Message** | `message:received` · `message:sending` · `message:sent` · `message:failed` · `message:ack` · `message:persisted` · `message:deleted` | | **Webhook** | `webhook:before` · `webhook:queued` · `webhook:delivered` · `webhook:after` · `webhook:error` | | **Ingress** | `ingress:error` | ### Capabilities The `PluginContext` exposes a deliberately small surface. Every permission in the last column except `webhook:ingress` is checked at the call: invoking a capability you didn't declare throws `PluginCapabilityError` mid-run, not at install. `webhook:ingress` is the exception, in both directions. Calling `ctx.registerWebhook` without it never throws and never logs: the host drops the subscription and the route simply never fires. But declaring `ingress` in the manifest without it is a hard load failure for the whole plugin, not just that route: install answers HTTP 400, and at boot the directory is skipped and the plugin comes up ERROR. | Capability | Methods | Permission | | ---------- | ------- | ---------- | | `ctx.messages` | `sendText(session, chat, text)` · `reply(session, chat, quotedId, text)` | `messages:send` | | `ctx.engine` (read-only) | `getGroupInfo` · `getContacts` · `getContactById` · `checkNumberExists` · `getChats` · `getChatHistory` (0.8.6+) · `canonicalChatId` (0.8.7+) | `engine:read` | | `ctx.net` | `fetch(url, init)` — host-proxied, SSRF-guarded outbound HTTP (0.7+) | `net:fetch` + manifest `net.allow` | | `ctx.conversations` | `send(envelope)` — normalized outbound (text/image/file/audio/video/voice/location) (v1) | `conversation:send` | | `ctx.handover` | `set(…, state)` — bot/human/closed handover for a mapped chat (v1) | `conversation:send` | | `ctx.mappings` | `upsert` · `get` · `getByProvider` — WA-chat ↔ provider-conversation mapping (v1) | `conversation:send` | | `ctx.registerWebhook` | claim an inbound ingress route (v1) | `webhook:ingress` | | `ctx.storage` | `get` · `set` · `delete` · `list` (namespaced per plugin) | `storage:use` | | `ctx.logger` | `log` · `debug` · `warn` · `error` | — | Also available: `ctx.config`, `ctx.pluginId`, `ctx.registerHook`. > `ctx.storage` was ungated until OpenWA moved it behind `storage:use`. A plugin that persists anything > should declare it now rather than when its host is upgraded: an older host ignores an unrecognized > permission, so the declaration is free, and a newer one denies the call — mid-conversation, because > permissions are checked at the call and not at load. > > There is no `ctx.manifest` and no `ctx.hookManager` on the sandboxed context the worker builds, so > reading `ctx.manifest.version` typechecks against a stale copy of the vendored types and throws at > runtime. To know your own version, bake it in at build time (see `package.mjs`, which defines > `__PLUGIN_VERSION__` from the manifest). ### Constraints to design around - **Ship JavaScript, not TypeScript.** The loader `require()`s `main` directly with no transpile step. Author in TS and bundle to a single `dist/index.js`. - **`type` must be `extension`.** Engine, storage, queue, and auth plugins are first-party built-ins and cannot be installed at runtime. - **Package limits:** `.zip` ≤ 5 MB compressed, ≤ 200 files, ≤ 20 MB uncompressed. Bundle your dependencies — there is no `npm install` at install time. - **No published SDK package.** Vendor the OpenWA types (see [`types/openwa.d.ts`](./types/openwa.d.ts)); they are the de-facto contract. - **Sandboxed worker.** Since OpenWA v0.6.0 each plugin runs in its own worker thread; capabilities and outbound HTTP are host-gated (`ctx.net.fetch` is SSRF-guarded; every capability is checked against the declared `permissions`). The worker still has Node APIs within its thread, so install only plugins you trust. - **Version floors are enforced since OpenWA v0.24.0.** The host rejects a malformed `minOpenWAVersion` or one newer than itself at install and boot load. Record the host version and test scope in the plugin's README. ## Authoring a plugin 1. **Scaffold** a new folder at the repo root (copy [`gsheets-logger`](./gsheets-logger) as a starting point). 2. **Write `manifest.json`** and an `index.ts` whose default export implements `IPlugin`. 3. **Keep host-free logic separate** from host adapters (see the example's split between pure mapping and the I/O client) so it stays unit-testable without OpenWA. 4. **Validate config defensively** inside `onLoad`/`onEnable` — the host does not enforce your `configSchema` types, only redacts `secret` fields. 5. **Build and package**, then install the resulting `.zip` into a local OpenWA instance to verify. ### Building from source ```bash # Bundle a plugin's TypeScript into a single dist/index.js and produce .zip node package.mjs gsheets-logger ``` The build bundles the entry to a single CommonJS file (exposing `.default`) with [esbuild](https://esbuild.github.io/) — a dev-only dependency — and zips it together with `manifest.json`. The shipped bundle uses only Node built-ins, so the package stays well within the install limits. ## Repository structure ``` OpenWA-plugins/ ├─ README.md ├─ LICENSE ├─ types/ │ └─ openwa.d.ts # vendored OpenWA plugin interfaces (the contract) ├─ package.mjs # `node package.mjs ` → bundle + zip └─ / # one folder per plugin ├─ manifest.json ├─ index.ts # default-exports an IPlugin class ├─ … # supporting modules + tests └─ dist/index.js # built artifact (manifest.main) ``` ## Compatibility All 11 plugins are checked against OpenWA **v0.30.0** using its package parser, manifest validators and worker runtime with isolated service doubles. Verification covers lifecycle calls, declared hooks, representative capability calls and per-session configuration where supported. Live WhatsApp sessions, provider credentials and dashboard rendering are outside that check. Each plugin declares its `minOpenWAVersion`, which remains its supported lower bound. OpenWA enforces this floor at install and boot load since v0.24.0; `testedOpenWAVersion` records the worker verification baseline and does not promise compatibility with every host configuration. ## Contributing Contributions are welcome — new plugins, fixes, and documentation. 1. Fork and branch from `main`. 2. Add your plugin in its own folder following the [authoring guide](#authoring-a-plugin), including a per-plugin `README.md` with setup steps and a config reference. 3. Include at least one runnable test for non-trivial logic. 4. Ensure the plugin builds and packages cleanly with `node package.mjs `. 5. Open a pull request describing what the plugin does and the OpenWA version you tested against. Please keep plugins focused, dependency-light, and least-privilege — declare only the permissions and session scope you actually need. ## Security Plugins run in a **sandboxed worker thread** (since OpenWA v0.6.0): capabilities and outbound HTTP are host-gated (`ctx.net.fetch` is SSRF-guarded; every capability is checked against the declared `permissions`). The worker still has Node APIs within its thread, so treat every plugin as trusted code: - Only install plugins from sources you trust. - Review the manifest's `permissions` and `sessions` before enabling. - Store secrets via the dashboard's `secret`-flagged config fields, never in source. Found a security issue in a plugin here? Please report it privately via the [OpenWA security policy](https://github.com/rmyndharis/OpenWA/security) rather than opening a public issue. ## Support If these plugins are useful to you, you can support OpenWA development by buying me a coffee. Buy Me a Coffee ## License [MIT](LICENSE) © Yudhi Armyndharis & OpenWA Contributors.
Built for OpenWA — the free, self-hosted WhatsApp API Gateway.