open-wa
DocsCustom code

Custom code

Install open-wa, create a complete Node.js consumer, and handle readiness, messages, and shutdown.

Use embedded custom code when your Node.js process owns the browser and session lifecycle. The examples target the supported 5.1.0 packages; the SocketClient path is separate and connects to an already running Easy API process.

Use Node.js 22.21.1 or newer with npm. Check your version with node --version before creating the project.

Minimal JavaScript project

Create a project, install the runtime and Puppeteer driver, then create index.mjs:

mkdir open-wa-bot
cd open-wa-bot
npm init -y
npm pkg set type=module
npm install @open-wa/wa-automate@5.1.0 @open-wa/driver-puppeteer@5.1.0

Save this as index.mjs:

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

const runtime = await createClient({
  sessionId: 'hello-bot',
  driver: new PuppeteerDriver(),
  headless: false,
});

const client = new Client({
  client: runtime,
  transport: runtime.getTransport(),
});

await client.start();
console.log('open-wa is ready:', runtime.getReadiness().ready);

client.onMessage(async (message) => {
  if (message.body === 'Hi') {
    const result = await client.sendText(message.from, '👋 Hello from custom code.');
    console.log('reply sent:', result);
  }
});

const shutdown = async (signal) => {
  console.log(`Stopping after ${signal}...`);
  await client.stop(signal);
};

process.once('SIGINT', () => void shutdown('SIGINT'));
process.once('SIGTERM', () => void shutdown('SIGTERM'));

Run it with:

node index.mjs

On the first run, scan the QR code shown in the visible browser window. Successful startup is the log line open-wa is ready: true; then send Hi to the linked account and look for reply sent:. The listener reuses the exact message.from chat ID, so it also works when the runtime supplies an @lid identifier.

TypeScript project

Install the TypeScript runner and types in the same project:

npm install --save-dev tsx typescript @types/node
npm pkg set type=module
mkdir -p src

Save this as src/index.ts:

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

async function main(): Promise<void> {
  const runtime = await createClient({
    sessionId: 'hello-bot-ts',
    driver: new PuppeteerDriver(),
    headless: false,
  });

  const client = new Client({
    client: runtime,
    transport: runtime.getTransport(),
  });

  await client.start();
  const readiness = runtime.getReadiness();
  if (!readiness.ready) {
    throw new Error(`WhatsApp session is not ready: ${readiness.pending.join(', ') || 'unknown blocker'}`);
  }

  console.log(`open-wa is ready for ${runtime.sessionId}`);

  client.onMessage(async (message) => {
    if (message.body !== 'Hi') return;

    const result = await client.sendText(message.from, '👋 Hello from TypeScript.');
    console.log('reply sent:', result);
  });

  let stopping = false;
  const shutdown = async (signal: string): Promise<void> => {
    if (stopping) return;
    stopping = true;
    console.log(`Stopping after ${signal}...`);
    await client.stop(signal);
  };

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

main().catch((error: unknown) => {
  console.error('open-wa failed to start:', error);
  process.exitCode = 1;
});

Run the saved file with:

npx tsx src/index.ts

This variant checks runtime readiness before registering the reply flow, stops the browser on signals, and reports startup failures through the process exit code.

Choose another browser driver

The examples use PuppeteerDriver so the install and run path is complete. To use Lightpanda, install the matching driver package and change only the driver import and constructor:

npm install @open-wa/driver-lightpanda@5.1.0
import { LightpandaDriver } from '@open-wa/driver-lightpanda';

const runtime = await createClient({
  sessionId: 'hello-bot',
  driver: new LightpandaDriver(),
});

Keep the rest of the lifecycle and readiness code unchanged. If you need to attach to an existing browser, set the driver and its connection options explicitly; do not mix embedded createClient ownership with SocketClient ownership in the same process.

Pass linkCode to createClient when the account uses phone-number pairing:

const runtime = await createClient({
  sessionId: 'hello-bot',
  driver: new PuppeteerDriver(),
  headless: false,
  linkCode: '447123456789',
});

Use digits only, and enter the generated code on the phone that owns that number. See Link-code login for the two-device sequence and fallback behavior.

Embedded runtime versus SocketClient

Embedded code creates the browser with createClient, wraps it in the exported Client facade, and calls client.start() and client.stop(). SocketClient connects to an Easy API process that owns the browser, so install and run that server separately and give the client its URL and API key. Choose one owner for each session so two processes do not compete for the same profile.

Next steps

Was this helpful?

Your answer includes the page path and docs version.

On this page