Skip to content

About

Official & community plugins for OpenWA — the open-source WhatsApp API Gateway.

Resources

Stars

76 stars

Watchers

0 watching

Forks

Repository files navigation

OpenWA Plugins

Official & community plugins for 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 Built for OpenWA Plugin type PRs welcome Buy Me a Coffee

Install a plugin · Plugin catalog · Write your own · 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 Auto-replies with a configurable away/closing message to messages received outside business hours. 0.2.12 stable
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.3 beta
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.15 stable
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.14 stable
faq-bot Auto-replies to inbound WhatsApp messages from configurable FAQ keyword/regex rules. 0.2.17 stable
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.14 stable
gsheets-logger Logs WhatsApp message events to a Google Sheet via a service account. 0.3.15 stable
http-action Triggers safe REST API requests from WhatsApp commands and renders JSON responses back to chat. 0.2.15 stable
supabase-otp-hook Deliver Supabase Auth phone OTPs over WhatsApp. 0.3.13 beta
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.6 stable
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.4 beta

The table above is generated from each plugin's manifest.json + CHANGELOG.md by npm run catalog (and mirrored in plugins.json). See 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 ✅ Supported All fields per session; takes effect on next message.
ai-responder ✅ Supported All fields per session (different providers, models and prompts); rate limits are in memory and reset on restart.
chat-flow ✅ Supported All fields per session; flow state is per (session, chat).
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 ✅ Supported All fields per session (different rule sets per number).
group-translate ⚠️ Supported, with caveat Config-signature caching; multi-backend isolation needs one instance per session.
gsheets-logger ❌ Not supported Single-buffer single-sink design; use one instance per session.
http-action ✅ Supported All fields per session (different endpoints/action sets).
supabase-otp-hook ✅ Supported All fields per session; applies to the instance's bound session.
typebot-connector ✅ Supported All fields per session; flow state is per (session, chat).
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 or contribute one.

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

Replace https://your-openwa-host with your gateway's base URL and <ADMIN_API_KEY> with an admin-scoped key.

1. Build the plugin package (see Building from source) to get gsheets-logger.zip, then upload it:

curl -X POST "https://your-openwa-host/api/plugins/install" \
  -H "X-API-Key: <ADMIN_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):

curl -X PUT "https://your-openwa-host/api/plugins/gsheets-logger/config" \
  -H "X-API-Key: <ADMIN_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.

curl -X POST "https://your-openwa-host/api/plugins/gsheets-logger/enable" \
  -H "X-API-Key: <ADMIN_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

{
  "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.

import type { IPlugin, PluginContext } from "../types/openwa";

export default class MyPlugin implements IPlugin {
  async onEnable(ctx: PluginContext): Promise<void> {
    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<void> {
    // 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<T>) => Promise<HookResult<T>>, 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); 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 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

# Bundle a plugin's TypeScript into a single dist/index.js and produce <plugin>.zip
node package.mjs gsheets-logger

The build bundles the entry to a single CommonJS file (exposing .default) with esbuild — 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 <plugin>` → bundle + zip
└─ <plugin>/             # 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.27.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, 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 <plugin>.
  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 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 © Yudhi Armyndharis & OpenWA Contributors.

Built for OpenWA — the free, self-hosted WhatsApp API Gateway.

About

Official & community plugins for OpenWA — the open-source WhatsApp API Gateway.

Resources

Stars

76 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages