Learning objectives
After reading this article you will be able to:
- Choose between an unofficial library and Meta's official Cloud API
- Run a Baileys bot that saves its session and reconnects on its own
- Check that the bot is online and fix the most common errors
A WhatsApp bot is a program that receives messages sent to your number, decides what to answer and sends the reply. To work all day, it needs a live process and an authenticated session that survives restarts. Those are the two places where most bots break.
This guide covers both paths: Baileys, an unofficial library that connects the way WhatsApp Web does, and the Cloud API, Meta's official interface. The examples use the current Active LTS of Node.js and the Baileys 7 API. If npm install brings a different major version, read the project's release notes before you continue.
Unofficial library or Cloud API?#
| Baileys (unofficial) | Cloud API (official) | |
|---|---|---|
| How it connects | WebSocket, as a device linked to your number | HTTPS webhook + Graph API calls |
| Session | Auth folder on disk | Access token, no session |
| Server | Always-on process | Public HTTPS server with a valid certificate |
| Messaging first | Free-form, but ban risk | Approved templates only |
| Risk | Ban for violating the Terms of Service | No such risk, but needs a business account |
The Cloud API delivers incoming messages by webhook. When a user writes to you, a 24-hour customer service window opens, and it resets with each new message from them. Inside it you can reply with free-form text. Outside it, only with pre-approved templates. Meta charges per delivered template message, and free-form messages inside the window are not charged. Current rates and billing rules are on Meta's pricing page.
For a deeper library comparison, see Baileys, whatsapp-web.js or Cloud API. To build the bot's logic, see how to make a WhatsApp bot.
What does running a WhatsApp bot 24/7 mean?#
It means a process that runs nonstop on a machine that never shuts down, with the session saved so that a restart does not require pairing again. In Baileys, the session is the keys and credentials written to the auth folder. If that folder disappears, the bot becomes a new device and must be paired again.
That is why your laptop is fine for testing but not for production. It is also why disk matters as much as the process: in environments where the disk is wiped on every deploy, the bot loses its session on every update.
How do you run a bot with Baileys?#
Create the project and install the dependencies:
npm init -y
npm pkg set type=module
npm install baileysBaileys 7 is an ES module, hence type=module. Set two environment variables: WA_PHONE, the bot's number with digits only and the country code (for example 15551234567), and optionally AUTH_DIR, the session folder. Do not hard-code the number or credentials.
Create index.js:
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
} from "baileys";
const AUTH_DIR = process.env.AUTH_DIR ?? "auth_info";
const PHONE = process.env.WA_PHONE;
async function start() {
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
const sock = makeWASocket({ auth: state });
sock.ev.on("creds.update", saveCreds);
sock.ev.on("connection.update", async ({ connection, lastDisconnect, qr }) => {
// First run: no saved session, so request a pairing code.
if (qr && !sock.authState.creds.registered) {
if (!PHONE) return console.error("Set WA_PHONE to pair.");
console.log("Pairing code:", await sock.requestPairingCode(PHONE));
}
if (connection === "open") console.log("Connected to WhatsApp.");
if (connection === "close") {
const code = lastDisconnect?.error?.output?.statusCode;
if (code === DisconnectReason.loggedOut) {
console.error("Session ended from the phone. Delete the folder and pair again.");
process.exit(1);
}
if (code === DisconnectReason.connectionReplaced) {
console.error("Another instance took over the session. Exiting.");
process.exit(1);
}
start(); // network drop or restart requested by WhatsApp: reconnect
}
});
sock.ev.on("messages.upsert", async ({ messages, type }) => {
if (type !== "notify") return;
for (const msg of messages) {
if (msg.key.fromMe || !msg.message) continue;
const text =
msg.message.conversation ?? msg.message.extendedTextMessage?.text;
if (text?.trim().toLowerCase() === "!ping") {
await sock.sendMessage(msg.key.remoteJid, { text: "pong" });
}
}
});
}
start();Run it with WA_PHONE=15551234567 node index.js. On the phone, open Linked devices in WhatsApp, choose to link with a phone number, and type the code that appeared in the log. Once connected, send !ping from another number: the bot replies pong.
In earlier versions the printQRInTerminal option printed the QR code for you. In Baileys 7 it is deprecated and no longer prints anything: the QR arrives in the qr field of the connection.update event so you can render it your own way. The pairing code skips the QR and is easier to read in a server log.
How does the session survive restarts?#
useMultiFileAuthState writes credentials and keys to the auth folder, and the creds.update event calls saveCreds whenever something changes. On the next run Baileys reads the folder and connects with no QR or code. The project's documentation warns that keys must be saved every time they change, otherwise messages stop reaching the recipient.
Three precautions:
- Do not delete or overwrite the folder on a deploy. Keep it out of the ZIP and the repository.
- Treat the folder like a password. Anyone who copies it controls the session.
- One instance per session. Two copies of the bot with the same credentials knock each other offline.
useMultiFileAuthState is only an example: the Baileys source does not recommend it for production, with the caveat that it may do for a bot. For a larger project, store the state in a database.
How do you use the official Cloud API?#
The Cloud API has no session and no pairing. You need a Meta business account, a business phone number and a server with valid HTTPS (self-signed certificates are not supported). See how Let's Encrypt works if you need a certificate.
Meta verifies the endpoint with a GET request: if hub.verify_token matches your value, you answer 200 with the hub.challenge. Messages arrive by POST, signed in the X-Hub-Signature-256 header. A minimal server using only the Node.js standard library:
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const {
VERIFY_TOKEN,
APP_SECRET,
WA_TOKEN,
PHONE_NUMBER_ID,
GRAPH_API_VERSION,
PORT = "8080",
} = process.env;
if (!GRAPH_API_VERSION) throw new Error("Set GRAPH_API_VERSION (vXX.0 format) to a version from the Graph API changelog");
async function reply(to, body) {
await fetch(`https://graph.facebook.com/${GRAPH_API_VERSION}/${PHONE_NUMBER_ID}/messages`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${WA_TOKEN}`,
},
body: JSON.stringify({
messaging_product: "whatsapp",
to,
type: "text",
text: { body },
}),
});
}
createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname !== "/webhook") return res.writeHead(404).end();
if (req.method === "GET") {
const ok =
url.searchParams.get("hub.mode") === "subscribe" &&
url.searchParams.get("hub.verify_token") === VERIFY_TOKEN;
return res
.writeHead(ok ? 200 : 403)
.end(ok ? url.searchParams.get("hub.challenge") : "");
}
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks);
const expected = Buffer.from(
"sha256=" + createHmac("sha256", APP_SECRET).update(raw).digest("hex"),
);
const got = Buffer.from(req.headers["x-hub-signature-256"] ?? "");
if (got.length !== expected.length || !timingSafeEqual(got, expected)) {
return res.writeHead(401).end();
}
res.writeHead(200).end(); // answer fast; Meta retries failed deliveries
for (const entry of JSON.parse(raw).entry ?? [])
for (const change of entry.changes ?? [])
for (const m of change.value?.messages ?? [])
if (m.type === "text") await reply(m.from, `You said: ${m.text.body}`).catch(console.error);
}).listen(Number(PORT));Meta retries failed deliveries for up to 7 days, so the same event can arrive twice. In production, store each message id and ignore repeats. Variables such as WA_TOKEN and APP_SECRET come from the environment: see what environment variables are. So does the Graph API version, because each version stops working about two years after release: put the latest one from the Graph API changelog in GRAPH_API_VERSION, in vXX.0 format, and when it ages, change the value and restart.
How do you know the bot is online?#
- Send a test message (
!ping) and check the reply. - With Baileys, look for
Connected to WhatsApp.in the log and watch for repeated disconnects. - With the Cloud API, use the webhook test in Meta's dashboard and confirm your server answers 200.
- Make sure the process restarts on its own if it stops. On a VPS, that is the job of
systemdorpm2.
What are the common errors?#
- The bot asks for a code on every restart: the auth folder was deleted or the disk is not persistent. Store the folder somewhere that survives deploys.
- Session ended (
loggedOut): the device was removed under Linked devices, or WhatsApp invalidated the session. Delete the folder and pair again. - Two bots knocking each other offline (
connectionReplaced): another instance uses the same credentials, possibly on your PC. Keep only one. See also why a WhatsApp bot disconnects. - Cloud API webhook fails verification:
hub.verify_tokendiffers from the configured one, the URL has no valid HTTPS or the route is wrong. - Message rejected outside the 24-hour window: only approved templates can start a conversation after the window closes.
Cannot use import statement outside a module:"type": "module"is missing frompackage.json.
How to do this on Square Cloud#
Square Cloud is self-service hosting: you upload the project, set the memory and control everything from the dashboard. It works with Baileys, WPPConnect, whatsapp-web.js and the official Cloud API. The auth folder lives on the application's disk, so the session stays connected after a restart.
Containers are always on, with no sleep mode and automatic restart (on by default for dashboard deploys) when the process exits with an error. WA_PHONE, WA_TOKEN, GRAPH_API_VERSION and the other variables are set per application in the dashboard, and real-time logs show the pairing code and connection events. You deploy by ZIP, CLI, GitHub or the VS Code extension, and squarecloud.app is optional: the dashboard generates the file with runtime, memory and main file, or you write it at the project root.
All plans are paid and billed in Brazilian reais. Hobby costs R$12.49 per month, with 1 GB of RAM shared across your applications, and the minimum per background application, such as a bot, is 256 MB. Web applications, such as a Cloud API webhook, start at 512 MB and get a squareweb.app subdomain with HTTPS. See the WhatsApp bot hosting page.
Bot hosting
Keep your bot online 24/7 with automatic restarts
FAQs
Does a WhatsApp bot need my computer to stay on?
It needs a process running somewhere. If that is your PC, the bot goes down when the PC shuts off or loses internet. On an always-on server it keeps answering without depending on your machine.
Do I have to pair the bot every time it restarts?
No, as long as the auth folder is saved on disk and not deleted. Baileys reads the credentials from that folder and reconnects without pairing again. You only pair again if the session is ended from the phone.
Do I need a domain to host a WhatsApp bot?
Not with Baileys, because the bot opens the connection itself. With the official Cloud API you do: Meta delivers messages by webhook, which needs a public URL with HTTPS and a valid certificate.
Can using Baileys get my number banned?
Yes. Unofficial libraries are not authorized by WhatsApp, and the Baileys maintainers say they do not condone uses that violate WhatsApp's Terms of Service, leaving the responsibility with each user. For commercial use, the official Cloud API avoids that risk.
More in Bots
See all 33 articles- How much does it cost to host a Discord bot?The cost of hosting a Discord bot depends on RAM, uptime, database and outside services. Learn how to estimate your own bill and where the money goes.
- Running a bot on your phone or PC vs hosting itA bot on your PC or phone (Termux) goes offline when the device shuts down, sleeps or loses its network. Compare the three options and when each one fits.
- How to host a Discord bot 24/7To keep a Discord bot online 24/7, run its process on an always-on machine with automatic restarts. Compare your PC, a VPS with pm2 or systemd, and bot hosting.
- Why your Discord bot keeps going offline (and how to fix it)A Discord bot goes offline when the process dies, the token is invalid, an intent is disabled or memory runs out. Learn to diagnose each cause and fix it.