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

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-puppeteerIf 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-puppeteerThen 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.
Recommended next steps
- Move session-specific settings into a real config object.
- Read Configuration and CLI for the core options.
- Read Session events if you need QR or lifecycle hooks.
- Read Best practices before production rollout.
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
evlisteners when you need raw lifecycle hooks. - Externalize heavy work to queues or workers instead of doing everything inside one
onMessagecallback. - Read Error handling and Best practices before shipping a long-running bot.

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