Plugin getting started
Build a small plugin that listens for a message, checks required fields, and sends a reply.

Plugin getting started
Use a plugin when you want reusable logic to run inside the Easy API process. A plugin can listen for events, call client methods, mount HTTP routes, add dashboard pages, or register AI tools.
What plugins can do
A plugin is an npm package (or local file) that:
- Listens to WhatsApp events (messages, auth, lifecycle)
- Calls WhatsApp methods (send messages, decrypt media)
- Mounts HTTP routes at
/plugins/<name>/ - Adds pages to the dashboard sidebar
- Registers tools for AI agents to call
Plugins run inside the Easy API process, not as separate services. They receive a security-filtered event emitter and a transport-agnostic client proxy.
Pick the right extension point
| Use a plugin when... | Use something else when... |
|---|---|
| Building reusable integrations | You need a separate process (use SocketClient) |
| Sharing functionality across sessions | You need direct browser control (use createClient) |
| Adding dashboard pages | You only need simple event forwarding (use the webhook integration plugin) |
| Exposing AI tools via MCP | You need low-level runtime ownership (use embedded runtime) |
Install the SDK
pnpm add @open-wa/plugin-sdk
# or
npm install @open-wa/plugin-sdkThe SDK provides:
createPlugin(): Type-safe plugin factorydefineConfig(): Zod wrapper for plugin configurationz: Zod (re-exported for convenience)- TypeScript types for all plugin contracts
Build your first plugin
Create the plugin
Use the createPlugin() factory:
import { createPlugin } from '@open-wa/plugin-sdk';
function getTextMessage(message: unknown) {
if (!message || typeof message !== 'object') return null;
const candidate = message as { body?: unknown; from?: unknown };
if (typeof candidate.body !== 'string' || typeof candidate.from !== 'string') {
return null;
}
return { body: candidate.body, from: candidate.from };
}
export default createPlugin({
meta: {
name: 'greeting-bot',
version: '1.0.0',
description: 'Greets new contacts with a welcome message',
},
init: async ({ client, logger }) => ({
'message.received': async ({ message }) => {
const msg = getTextMessage(message);
if (!msg) return;
if (msg.body === 'Hi') {
await client.sendText(msg.from, '👋 Welcome! How can I help?');
logger.info('Sent greeting', { from: msg.from });
}
},
}),
});Define configuration
Use defineConfig() with Zod for typed configuration:
import { createPlugin, defineConfig, z } from '@open-wa/plugin-sdk';
function getTextMessage(message: unknown) {
if (!message || typeof message !== 'object') return null;
const candidate = message as { body?: unknown; from?: unknown };
if (typeof candidate.body !== 'string' || typeof candidate.from !== 'string') {
return null;
}
return { body: candidate.body, from: candidate.from };
}
const config = defineConfig(z => z.object({
greeting: z.string().default('👋 Welcome!'),
triggerWord: z.string().default('Hi'),
}));
export default createPlugin({
meta: { name: 'greeting-bot' },
configSchema: config,
init: async ({ client, logger, config }) => ({
'message.received': async ({ message }) => {
const msg = getTextMessage(message);
if (!msg) return;
if (msg.body === config.triggerWord) {
await client.sendText(msg.from, config.greeting);
}
},
}),
});Add event handlers
The init function returns a Hooks object. Available hooks include:
'message.received': Incoming messages'message.sent': Outgoing messages'core.started': After the session is ready'auth.qr': When a QR code is emitted'dispose': On session shutdown
See the Hooks reference for the complete list.
Put the files in one project
Use a layout where the config file and plugin file live next to each other:
my-openwa-app/
wa.config.mjs
plugins/
greeting-bot.mjsPut the plugin code from Step 2 in plugins/greeting-bot.mjs. The plugin loader uses dynamic import(ref), so a local plugin should be loadable JavaScript at runtime. Do not point the runtime at ./plugins/greeting-bot.ts unless your own Node.js startup environment already installs a TypeScript loader; the current plugin loader does not compile plugin TypeScript for you.
Load the plugin
In wa.config.mjs, build the plugin reference from the config file location:
// wa.config.mjs
export default {
sessionId: 'my-session',
plugins: [
new URL('./plugins/greeting-bot.mjs', import.meta.url).href,
],
pluginConfig: {
'greeting-bot': {
greeting: 'Hello! Welcome to our service.',
triggerWord: 'hello',
},
},
};The plugins array accepts npm package names, scoped package names, absolute local references, and local references that Node can import. The module must export a plugin as its default export or as a named plugin export. The pluginConfig object uses the plugin name from meta.name, not the package name or file path.
Run from the project directory
Run the CLI from my-openwa-app/ or pass the config path explicitly:
npx @open-wa/wa-automate --config ./wa.config.mjs --port 8080If this Easy API instance is reachable outside your machine, add --api-key "your-secure-key" so plugin-mounted routes and the main API share the same authentication boundary.
Expected local startup logs are illustrative, not captured from this docs update. A successful load should include plugin loader and registration messages similar to:
plugin_loaded { ref: 'file:///.../plugins/greeting-bot.mjs', name: 'greeting-bot', version: '1.0.0' }
plugin_registered { plugin: 'greeting-bot', version: '1.0.0' }After the session is connected, send the trigger word from another WhatsApp account:
helloExpected local behavior: the plugin sends Hello! Welcome to our service. back to the sender and logs the greeting action.
Expected plugin failure logs are illustrative and abbreviated. A missing file or invalid config should fail before the plugin handles messages and show a signal similar to:
plugin_load_error { ref: 'file:///.../plugins/greeting-bot.mjs', error: 'Cannot find module' }
plugin_config_invalid { plugin: 'greeting-bot', issues: [...] }If the plugin does not load
- Confirm the CLI loaded the intended config file. Run with
--config ./wa.config.mjswhen in doubt. - Confirm the plugin reference points to JavaScript that Node can import, such as
.mjsor compiled.js. - Confirm the module exports
default createPlugin(...)or a namedpluginexport. - Confirm
meta.nameis present. Missing names are skipped with aplugin_load_skipwarning. - Confirm
pluginConfigis keyed bymeta.name, for example'greeting-bot'. - If the greeting never appears, send the exact configured
triggerWord, then check forplugin_loaded,plugin_registered,plugin_config_invalid, orplugin_load_errorin the Easy API logs. After the load succeeds, move reusable behavior into a package and read Publishing a plugin.
Plugin architecture
Security-filtered event emitter
Plugins receive a filtered event emitter that:
- Can subscribe to public events (
on,once,off) - Cannot emit events (only the host can emit)
- Cannot listen to internal events (
launch.*,browser.*,transport.*) - Cannot listen to sensitive events (
license.*, session data)
Transport-agnostic client proxy
Plugins get a client proxy that forwards method calls to the WhatsApp runtime. This is not direct browser access. It works through the host's transport layer (HTTP RPC / SSE).
Config validation
If you provide a configSchema, the host validates the plugin's config before calling init(). If validation fails, the plugin will not load and an error is logged.
When to use plugins
- Building reusable integrations (CRM bridges, webhook forwarders)
- Adding custom message processing (moderation, translation, transcription)
- Exposing HTTP endpoints alongside the API
- Adding dashboard pages for monitoring
- Registering tools for AI agents
Related
- Hooks reference: Complete hooks API
- PluginClient reference: Available client methods
- PluginInput breakdown: What your plugin receives
- Plugin security model: Boundaries and restrictions
- Publishing a plugin: How to share your plugin

Was this helpful?
Wally and his cute companion coffee mug are coding day and night to keep this up-to-date!
