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

Custom code

Use createClient in your Node.js app, choose a driver, and handle startup and shutdown safely.

Custom-code Wally

Custom code

Use this path when your Node.js app needs to own the open-wa runtime instead of calling a separate Easy API process.

Install the package and a driver

pnpm add @open-wa/wa-automate @open-wa/driver-puppeteer

If your environment has stricter native-install permissions, only fall back to --unsafe-perm when you have confirmed you actually need it.

Runtime entrypoint

The v5 public contract centers on createClient from @open-wa/core, exposed through @open-wa/wa-automate. Start new v5 custom code with createClient because it matches the current runtime and driver architecture.

import { createClient } from '@open-wa/wa-automate';
import { PuppeteerDriver } from '@open-wa/driver-puppeteer';

async function start() {
  const client = await createClient({
    sessionId: 'my-session',
    driver: new PuppeteerDriver(),
    headless: true,
  });

  client.onMessage(async (message) => {
    if (message.body === 'Hi') {
      await client.sendText(message.from, '👋 Hello!');
    }
  });
}

start().catch(console.error);

On first run, authenticate the session by scanning the QR code or using another supported login flow. When the session is ready, send Hi to the connected account and expect the reply above.

create() is the older compatibility entry point. It still appears in some v4-era examples, but it is not the first pattern to copy for new v5 code.

import { create } from '@open-wa/wa-automate';

async function start() {
  const client = await create({
    sessionId: 'my-session',
  });

  client.onMessage(async (message) => {
    if (message.body === 'Hi') {
      await client.sendText(message.from, '👋 Hello!');
    }
  });
}

start().catch(console.error);

Choose the driver explicitly

For embedded runtime code, install the driver package you want to run with. The examples above use Puppeteer:

pnpm add @open-wa/driver-puppeteer

Then pass the driver when you create the client:

import { createClient } from '@open-wa/wa-automate';
import { PuppeteerDriver } from '@open-wa/driver-puppeteer';

async function start() {
  const client = await createClient({
    sessionId: 'my-session',
    driver: new PuppeteerDriver(),
    headless: true,
  });

  client.onMessage(async (message) => {
    if (message.body === 'Hi') {
      await client.sendText(message.from, '👋 Hello!');
    }
  });
}

start().catch(console.error);

TypeScript setup

Custom code projects work best with a normal Node.js TypeScript setup. Use a recent Node target, keep module resolution compatible with your runtime, and await async client methods instead of treating them like sync helpers.

What runtime creation does

createClient() launches the automation runtime, restores or authenticates the session, and returns the client surface you use for listeners and actions.

It is the boundary where configuration, browser launch, authentication state, and runtime listeners all meet.

Build a real launch config

const client = await createClient({
  sessionId: 'sales',
  driver: new PuppeteerDriver(),
  headless: true,
  qrTimeout: 0,
  authTimeout: 60,
});

Use an explicit driver and browser configuration when browser-specific behavior matters for your flow.

Auth behavior

On first run, the runtime needs to authenticate the WhatsApp session. Depending on your config and environment, that usually means scanning a QR code or using another supported login flow.

After the first successful login, the session data is saved for the configured sessionId. Later runs with the same session can restore that state instead of asking you to log in again, as long as the saved auth data is still valid and available to the process.

First send/receive

This example sends a startup message to a known chat and replies when that chat sends Hi:

import { createClient } from '@open-wa/wa-automate';
import { PuppeteerDriver } from '@open-wa/driver-puppeteer';

async function start() {
  const client = await createClient({
    sessionId: 'sales',
    driver: new PuppeteerDriver(),
    headless: true,
  });

  const chatId = '15551234567@c.us';

  await client.sendText(chatId, 'Bot is online. Send Hi to test the reply flow.');

  client.onMessage(async (message) => {
    if (message.from === chatId && message.body === 'Hi') {
      await client.sendText(message.from, '👋 Hello from custom code.');
    }
  });
}

start().catch(console.error);

Replace 15551234567@c.us with the chat id you want to test against.

Cleanup/shutdown

If your process handles shutdown signals, close the client before exiting so the browser and runtime resources are cleaned up:

import { createClient } from '@open-wa/wa-automate';
import { PuppeteerDriver } from '@open-wa/driver-puppeteer';

async function start() {
  const client = await createClient({
    sessionId: 'sales',
    driver: new PuppeteerDriver(),
  });

  const shutdown = async () => {
    await client.stop();
    process.exit(0);
  };

  process.once('SIGINT', shutdown);
  process.once('SIGTERM', shutdown);
}

start().catch(console.error);

v5 caveat

Plugin vs custom code

Use plugins when you are building reusable integrations that should be loaded through config, shared across projects, or published as separate packages.

Use custom code when the logic belongs to one app, such as routing incoming messages into your own database, queue, CRM, or business workflow.

SocketClient vs embedded runtime

SocketClient connects your Node.js app to a running Easy API instance. The Easy API process owns the browser, session lifecycle, and HTTP surface while your app consumes commands and events remotely.

Embedded runtime code owns the browser directly inside your process through createClient. Use it when you need direct control over driver selection, launch config, shutdown, and session behavior.

Browser/runtime notes

  • If you are using your own Chrome install, make that explicit instead of depending on auto-discovery.
  • On Linux hosts, validate browser dependencies before debugging application logic.
  • Keep the browser/runtime assumptions the same between local development and production where possible.

Runtime notes

  • Prefer explicit Chrome configuration when you rely on browser-specific behavior.
  • Keep one primary client per process unless you have a clear multi-session orchestration plan.
  • Treat all client methods as async operations and await them consistently.

Common next moves

  • Add ev listeners when you need raw lifecycle hooks.
  • Externalize heavy work to queues or workers instead of doing everything inside one onMessage callback.
  • Read Error handling and Best practices before shipping a long-running bot.
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