open-wa v5 is alpha. Use v4.76.0 for mature production systems unless you are validating v5.
The Client APIAPI ExplorerLicensing

Anatomy of a Plugin

Step-by-step walkthrough of the webhook and Chatwoot reference plugins.

Plugin Cutaway Wally

Anatomy of a Plugin

This page walks through two real plugins shipped with open-wa to show the patterns you should reuse when building your own.

The Webhook Plugin

The webhook plugin pushes WhatsApp events to an external HTTP endpoint. It demonstrates event forwarding, configuration, and HTTP route mounting.

File Location

integrations/webhook/src/plugin.ts

Structure Breakdown

export default createPlugin({
  meta: { name: 'webhook' },
  configSchema: webhookConfigSchema,
  init: async ({ events, logger, config, client }) => { ... },
  routes: () => { ... },
});

Key Patterns

  • meta.name: Used for logging prefix and config key mapping
  • configSchema: Zod schema that validates pluginConfig.webhook from wa.config.js
  • init: Sets up event listeners that forward WhatsApp events to the configured webhook URL
  • routes: Mounts a health check endpoint at /plugins/webhook/health

Event Forwarding Pattern

events.on('message.received', async ({ message }) => {
  await fetch(config.url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      event: 'message.received',
      data: message,
      timestamp: Date.now(),
    }),
  });
});

This pattern is reusable for any external service integration.

The Chatwoot Plugin

The Chatwoot plugin bridges WhatsApp into Chatwoot conversations. It demonstrates bidirectional message handling, contact management, and media mapping.

File Location

integrations/chatwoot/src/plugin.ts

Structure Breakdown

export default createPlugin({
  meta: { name: 'chatwoot' },
  configSchema: chatwootConfigSchema,
  init: async ({ events, logger, config, client }) => { ... },
});

Key Patterns

  • Bidirectional sync: WhatsApp messages flow into Chatwoot, and Chatwoot agent replies flow back to WhatsApp
  • Contact mapping: Phone numbers are mapped to Chatwoot contacts
  • Media handling: Media messages are uploaded to Chatwoot as attachments

Contact Sync Pattern

async function getOrCreateContact(phoneNumber: string) {
  // Check if contact exists in Chatwoot
  // If not, create a new contact
  // Return the contact ID
}

Message Routing Pattern

events.on('message.received', async ({ message }) => {
  const contactId = await getOrCreateContact(message.from);
  await createChatwootMessage(contactId, message.body);
});

Patterns to Reuse

1. Config Validation

Always provide a configSchema:

const configSchema = z.object({
  url: z.string().url(),
  apiKey: z.string().optional(),
  enabled: z.boolean().default(true),
});

2. Event Subscription

Subscribe to specific events rather than using catch-all:

events.on('message.received', handler);
events.on('client.ready', handler);
events.on('core.stopping', handler);

3. Error Handling

Wrap external calls in try-catch and log errors:

try {
  await fetchExternalAPI(data);
} catch (error) {
  logger.error('External API failed', { error: error.message });
}

4. Graceful Degradation

Continue processing even when external services fail:

events.on('message.received', async ({ message }) => {
  try {
    await processMessage(message);
  } catch (error) {
    logger.error('Processing failed', { error: error.message });
    // Message is not lost - it will be retried or logged
  }
});

5. Lifecycle Awareness

Handle session state changes:

events.on('core.stopping', async () => {
  // Flush queues, save state, close connections
  logger.info('Plugin shutting down');
});
Wally the Walrus typing

Was this helpful?

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

On this page