Skip to content

WhatsApp Bot With Node.js and JavaScript: Cloud API, Baileys and whatsapp-web.js

By Erfan Ahmed, founder of Erfan MD · Updated October 2026

Node.js is the most common choice for building a WhatsApp bot in JavaScript. You have three main routes: the official WhatsApp Business Cloud API, and two unofficial libraries, whatsapp-web.js and Baileys. This guide explains how each one works, shows starter code for all three, compares their risks, and covers sessions, hosting and common errors, so you can pick the right tool before you write much code.

Quick answer: For a business that depends on WhatsApp, use the official Cloud API with Express. For learning, groups and personal projects, Baileys (a WebSocket library) and whatsapp-web.js (a browser-based library) are popular, but they are unofficial and can lead to a banned number. Pick based on how much risk you can accept.

Three Ways to Build a WhatsApp Bot in JavaScript

OptionHow it connectsOfficial?Resource useMain catch
Cloud API + ExpressMeta sends messages to your webhookYesVery lightMeta setup, template rules and message charges
whatsapp-web.jsRuns WhatsApp Web in a headless browser (Puppeteer)NoHeavy: needs a browserBreaks when WhatsApp Web changes; ban risk
BaileysTalks to WhatsApp over a WebSocket as a linked deviceNoLightAPI changes between versions; ban risk

Baileys is the library behind many community "MD" bots, which is why you see searches like "WhatsApp bot Baileys GitHub". Both unofficial libraries state in their own documentation that WhatsApp does not allow bots or custom clients and that blocking is possible. Take that warning seriously.

Before You Start

  • Node.js LTS (version 18 or newer is a safe baseline) and npm.
  • A spare WhatsApp number for any unofficial library. Do not test on your main number.
  • For the Cloud API: a Meta developer app, phone number ID, access token and a public HTTPS URL. Our guide to creating a WhatsApp bot explains the Meta side.

Option 1: Cloud API With Express (Recommended for Business)

npm init -y
npm install express dotenv

Create index.js. It verifies the webhook, checks Meta's signature, and replies to text messages with Node's built-in fetch:

require('dotenv').config();
const crypto = require('crypto');
const express = require('express');

const app = express();
const { VERIFY_TOKEN, WA_TOKEN, WA_PHONE_ID, APP_SECRET } = process.env;
const GRAPH = 'v21.0'; // use the current version from Meta's docs

// keep the raw body so the signature can be checked
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

const MENU = 'Hi! Reply 1 for hours, 2 for prices, 0 for a person.';
const REPLIES = {
  '1': 'We are open Monday to Saturday, 9am to 6pm.',
  '2': 'Our prices are at example.com/prices.',
  '0': 'A team member will reply soon.',
};

async function sendText(to, body) {
  const res = await fetch(`https://graph.facebook.com/${GRAPH}/${WA_PHONE_ID}/messages`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${WA_TOKEN}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ messaging_product: 'whatsapp', to, type: 'text', text: { body } }),
  });
  if (!res.ok) console.error('Send failed:', await res.text());
}

app.get('/webhook', (req, res) => {
  if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === VERIFY_TOKEN) {
    return res.status(200).send(req.query['hub.challenge']);
  }
  res.sendStatus(403);
});

app.post('/webhook', async (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', APP_SECRET).update(req.rawBody).digest('hex');
  const sig = req.get('X-Hub-Signature-256') || '';
  if (sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.sendStatus(403);
  }
  res.sendStatus(200); // answer fast, then work

  const msg = req.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
  if (!msg) return;
  if (msg.type === 'text') {
    const text = msg.text.body.trim().toLowerCase();
    await sendText(msg.from, REPLIES[text] || MENU);
  } else {
    await sendText(msg.from, 'I can only read text for now. ' + MENU);
  }
});

app.listen(3000, () => console.log('Bot listening on port 3000'));

Use a tunnel such as ngrok to test locally, set the webhook URL and verify token in the Meta dashboard, and subscribe to the messages field. The logic matches our Python version, so you can compare the two.

Option 2: whatsapp-web.js

This library launches a headless Chrome browser, loads WhatsApp Web and listens to events. Setup is quick, which is why it is popular for experiments:

npm install whatsapp-web.js qrcode-terminal
const { Client, LocalAuth } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');

const client = new Client({ authStrategy: new LocalAuth() });

client.on('qr', (qr) => qrcode.generate(qr, { small: true }));
client.on('ready', () => console.log('Bot is ready'));

client.on('message', async (msg) => {
  if (msg.body.toLowerCase() === 'ping') {
    await msg.reply('pong');
  }
});

client.initialize();

Scan the QR code from Linked Devices on your spare phone. LocalAuth stores the session on disk so you do not scan every time. The cost is memory, since a full browser runs in the background, which makes it a poor fit for small free hosting plans.

Option 3: Baileys

Baileys connects directly over WebSocket without a browser, so it is lighter and faster. It is also a moving target, because the maintainers change its API between versions and newer releases may need ES module imports. Always follow the README for the version you install. A typical connection looks like this:

npm install @whiskeysockets/baileys pino
const {
  default: makeWASocket,
  useMultiFileAuthState,
  DisconnectReason,
} = require('@whiskeysockets/baileys');

async function start() {
  const { state, saveCreds } = await useMultiFileAuthState('auth_session');
  const sock = makeWASocket({ auth: state });

  sock.ev.on('creds.update', saveCreds);

  sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
    if (connection === 'close') {
      const code = lastDisconnect?.error?.output?.statusCode;
      if (code !== DisconnectReason.loggedOut) start(); // reconnect
      else console.log('Logged out: delete auth_session and pair again');
    } else if (connection === 'open') {
      console.log('Connected');
    }
  });

  sock.ev.on('messages.upsert', async ({ messages }) => {
    const m = messages[0];
    if (!m.message || m.key.fromMe) return;
    const text = m.message.conversation || m.message.extendedTextMessage?.text || '';
    if (text.toLowerCase() === 'ping') {
      await sock.sendMessage(m.key.remoteJid, { text: 'pong' });
    }
  });
}
start();

The auth_session folder holds your login. Treat it like a password: keep it out of GitHub, back it up, and never share it. If a project asks you to upload a session file to a stranger's server, that person can control your WhatsApp account.

Pairing code instead of QR

Many bots let you link with a pairing code rather than a QR scan. You enter your number with the country code, receive an 8-character code, and type it under Linked Devices > Link a Device > Link with phone number instead. Baileys exposes a method for requesting this code, but its exact usage changes by version, so check the README. Erfan MD uses the same pairing-code flow through its own server page.

Low-Code Option: Node-RED

Node-RED is a visual flow tool built on Node.js. Many people search "WhatsApp bot Node-RED" because it needs little code. With the official API, a flow can use an HTTP In node as the webhook, a Function or Switch node to choose a reply, and an HTTP Request node to call the messages endpoint. Community nodes for WhatsApp exist too, but check who maintains them and how they connect before trusting them with your number.

Project Structure That Scales

Once your bot has more than a few replies, split the code so each feature is easy to find:

  • index.js for the connection and webhook only.
  • commands/ with one small file per command, each exporting a name and a handler function.
  • lib/ for helpers such as sending messages, logging and rate limiting.
  • .env and .gitignore to keep secrets and sessions out of version control.

A simple command loader reads the commands folder at start-up and calls the handler whose name matches the first word of the message after your prefix. This is how most open-source bots handle thousands of commands.

Hosting a Node.js WhatsApp Bot

  • Cloud API bots only need to answer web requests, so a small server, a Node-friendly platform or a serverless host works.
  • Baileys bots need an always-on server and a session folder that survives restarts.
  • whatsapp-web.js bots need extra memory for the browser.

Run any long-lived bot under PM2 (pm2 start index.js --name wa-bot) or Docker so it restarts after a crash. Our deployment guide has the full commands.

Finding Node.js Bot Code on GitHub

GitHub has hundreds of JavaScript WhatsApp bots. Before you run one, check what connection it uses, when it was last updated, what its dependencies are, and where it sends data. Read the code that touches your session or tokens, and be wary of obfuscated files or install scripts that download unknown code. Forks with the same code and new branding are common, so look at the original project.

Common Problems and Fixes

ProblemLikely causeFix
QR code or pairing failsOld linked device or wrong number formatRemove old linked devices and try again with the country code
Bot logs out after restartSession folder not savedKeep the auth folder on a persistent disk or volume
Frequent disconnectsSleeping free host or two sessions on one numberUse an always-on server and one session per number
whatsapp-web.js crashesBrowser missing dependencies or low memoryInstall Chrome libraries and use a larger server
Library stops working after an updateWhatsApp changed its protocolUpdate the library or pin a known working version
Cloud API webhook returns 403Verify token or signature mismatchMatch the token and use the raw body for the signature

Risks You Should Understand

Be aware: Baileys and whatsapp-web.js are not official WhatsApp products. WhatsApp and Meta decide whether a number is restricted or banned, and no developer or provider can control that. Use a spare number, keep message volumes low and only message people who expect to hear from you. For customer-facing business, use the official Cloud API.
  • Do not bulk message strangers. It is the fastest way to get banned.
  • Protect the session and tokens. Anyone with them can act as your number.
  • Tell people it is a bot, and honor "stop" requests.

Related guides: WhatsApp bot with Python, WhatsApp bot GitHub guide, pair code guide and API pricing.

Frequently Asked Questions

What is the best library for a WhatsApp bot in Node.js?

For business, the official Cloud API with Express. For personal or group projects, Baileys is lighter than whatsapp-web.js, but both are unofficial and risky.

Is Baileys safe to use?

The library is not malware, but it is unofficial and accounts using it can be banned. The bigger danger is untrusted bots built on it that steal sessions, so read any code you run.

Can I run a Node.js WhatsApp bot for free?

You can build and test for free. Free hosting often sleeps or has limits, so it is unreliable for a bot that must stay connected.

Which is lighter, Baileys or whatsapp-web.js?

Baileys, because it does not run a browser. whatsapp-web.js needs far more memory.

Do I need to code to make a WhatsApp bot?

No. Tools like n8n, Node-RED or no-code platforms let you build a basic bot with blocks.

Want a ready bot without the coding?

Erfan MD links to your WhatsApp with a pairing code and handles replies, broadcasts and follow-ups for you.

Explore Erfan MD