• Home
  • Blog
  • How to Build a WhatsApp Bot in 2026 (Step-by-Step)
Guides
12 min read May 28, 2026

How to Build a WhatsApp Bot in 2026 (Step-by-Step)

A practical end-to-end tutorial for building a WhatsApp bot in 2026 — compare the official Cloud API against Baileys and whatsapp-web.js, then ship a working bot with real code, webhooks, media handling and honest compliance guidance.

Key takeaways
  • There are three realistic paths in 2026: Meta's official WhatsApp Cloud API (sanctioned, free dev sandbox, per-message pricing in production) and two unofficial libraries — Baileys (WebSocket, no browser) and whatsapp-web.js (Puppeteer + Chromium).
  • Only the Cloud API carries zero Terms-of-Service ban risk. Baileys and whatsapp-web.js are reverse-engineered, ship their own disclaimers, and have documented account bans — user reports describe bans sometimes after only a handful of messages.
  • For the official path you build a webhook: a GET handler that echoes hub.challenge, and a POST handler that verifies the X-Hub-Signature-256 HMAC with crypto.timingSafeEqual before reading messages.
  • The 24-hour customer service window lets you send free-form (and free) replies for 24h after each user message; outside it you must use a pre-approved template.
  • Use the Cloud API for anything production or commercial; reach for Baileys or whatsapp-web.js only for prototypes or personal projects where the ban risk is acceptable.

Three paths to a WhatsApp bot — pick before you code

Before writing a single line, decide which platform you are building on, because the choice dictates everything else: your code, your hosting, your costs, and whether your number can be banned. In 2026 there are three realistic ways to put a WhatsApp bot into the world.

The first is the official one — Meta's WhatsApp Cloud API, a hosted Graph API endpoint at graph.facebook.com. It is the only WhatsApp-sanctioned way to build a bot, it gives you a free test number and sandbox to develop against, and it carries no Terms-of-Service ban risk. The other two are unofficial, reverse-engineered libraries that automate a regular WhatsApp account: Baileys, a pure WebSocket client with no browser, and whatsapp-web.js, which drives WhatsApp Web inside a headless Chromium. Both are powerful and free, both let you automate an ordinary number, and both put that number at risk.

This guide compares all three honestly, then walks you through a concrete build on each side: an official Cloud API webhook bot, and a quick unofficial bot with Baileys. By the end you will know which path fits your project and have working code to start from.

Cloud API vs Baileys vs whatsapp-web.js

Here is the decision at a glance. Versions are a snapshot verified mid-2026 — check each project's releases page when you read this, since the unofficial libraries move quickly.

WhatsApp Cloud APIBaileyswhatsapp-web.js
OwnerMeta (official)WhiskeySocketswwebjs (orig. Pedro S. Lopez)
InstallREST / Graph APInpm i baileysnpm i whatsapp-web.js
ArchitectureMeta-hosted HTTP endpointWebSocket multi-device, no browserPuppeteer + Chromium (WhatsApp Web)
LicenseHosted service (Meta ToS)MITApache-2.0
Latestper-message pricing model7.0.0 line (rc13)v1.34.7 (Apr 2026)
AuthAccess token + verified numberQR or pairing codeQR scan
FootprintNone (you call an API)Lightweight (Node 17+, ESM)Heavy (full Chromium per session)
Ban riskNone — sanctionedReal — documented bansReal — documented bans
Best forProduction, commercial, scaleLightweight prototypes, personalWeb-feature parity, don't mind Chromium

Path A — Official Cloud API: setup and sandbox

Start here if you are building anything production or commercial. The Cloud API is compliant, scalable, and free to develop against. The setup, all in the Meta dashboard at developers.facebook.com, is roughly: create a Meta app, add the WhatsApp product, and you are issued a free test phone number attached to a test WhatsApp Business Account (a sandbox). You can send and receive messages with that test number at no charge while you develop — no message billing until you move to a real number.

From the dashboard, grab three values you will use in code: a temporary access token, your test number's phone number ID, and your app secret (used to verify incoming webhooks). Going to production later requires a real number, Meta Business verification, and usually a Business Solution Provider — but none of that is needed to build and test the bot.

  1. Create a Meta app at developers.facebook.com and add the WhatsApp product.
  2. Copy the test phone number ID and the temporary access token from the API Setup panel.
  3. Add a recipient test number (your own phone) so the sandbox can message you.
  4. Note your App Secret under App Settings → Basic — you will need it to verify webhook signatures.

Path A — Receiving messages with a verified webhook

The Cloud API delivers incoming messages by calling a webhook URL you host. Setting it up has two halves. First, when you register the URL, Meta sends a GET request to verify you own it. It includes three query params: hub.mode (which will be 'subscribe'), hub.verify_token (a secret string you chose), and hub.challenge (a random value). You check the token matches, then echo hub.challenge back as plain text with a 200. Your endpoint must be HTTPS with a valid, non-self-signed TLS certificate.

JS
import express from 'express'
import crypto from 'crypto'

const app = express()
const VERIFY_TOKEN = process.env.VERIFY_TOKEN
const APP_SECRET = process.env.APP_SECRET

// Keep the raw body so we can verify the signature later.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } }))

// 1) Webhook verification (GET): echo hub.challenge back.
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode']
  const token = req.query['hub.verify_token']
  const challenge = req.query['hub.challenge']
  if (mode === 'subscribe' && token === VERIFY_TOKEN) {
    return res.status(200).send(challenge)
  }
  return res.sendStatus(403)
})

Second, real messages arrive as POST requests to the same URL. Meta signs every payload with an HMAC-SHA256 of the raw request body, keyed by your App Secret, in the header X-Hub-Signature-256: sha256=.... You must verify this before trusting the payload — and you must compare it with a timing-safe function (crypto.timingSafeEqual), never a plain === string comparison, which is vulnerable to timing attacks.

JS
// 2) Incoming messages (POST): verify HMAC, then read the message.
app.post('/webhook', (req, res) => {
  const signature = req.get('X-Hub-Signature-256') || ''
  const expected = 'sha256=' + crypto
    .createHmac('sha256', APP_SECRET)
    .update(req.rawBody)
    .digest('hex')

  const a = Buffer.from(signature)
  const b = Buffer.from(expected)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401)
  }

  const entry = req.body.entry?.[0]?.changes?.[0]?.value
  const msg = entry?.messages?.[0]
  if (msg) {
    console.log('From', msg.from, 'type', msg.type, 'text', msg.text?.body)
  }
  res.sendStatus(200) // Always 200 quickly so Meta doesn't retry.
})

app.listen(3000)

Path A — Replying, handling media, and the 24-hour window

To reply, POST to the Cloud API messages endpoint with your phone number ID and access token. Crucially, you can only send free-form replies within the 24-hour customer service window: after a user messages you, you have 24 hours to respond with any content for free, and each new inbound message resets the timer. Outside that window you must send a pre-approved template message instead — this is the single rule that trips up most beginners.

JS
async function sendText(to, body) {
  const PHONE_ID = process.env.PHONE_NUMBER_ID
  const TOKEN = process.env.WHATSAPP_TOKEN
  // Bump v21.0 to the current Graph API version when you build.
  await fetch(`https://graph.facebook.com/v21.0/${PHONE_ID}/messages`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messaging_product: 'whatsapp',
      to,                       // recipient in international format
      type: 'text',
      text: { body }            // free-form: only valid inside the 24h window
    })
  })
}

Media works in two directions. Incoming images, audio, documents, and video arrive in the webhook as a media ID (not the file itself); you make a follow-up call to resolve the ID to a temporary download URL, then fetch the bytes with your token. To send media, you first upload the file to get a media ID, then reference that ID in a message of type image, document, audio, or video. Keep handlers fast — acknowledge the webhook with a 200 immediately and do downloads or heavy work asynchronously so Meta doesn't time out and retry.

  • Inbound media: read msg.image.id (or msg.document.id, etc.) → GET the media URL → download with your bearer token.
  • Outbound media: upload the file to /{PHONE_ID}/media → receive a media id → send a message referencing { id }.
  • Default throughput is about 80 messages per second per number (auto-upgradable as your quality holds).

Path B — A quick bot with Baileys (unofficial)

If you need a throwaway prototype or a personal-project bot and accept the ban risk, Baileys is the lightest unofficial option. It speaks WhatsApp's multi-device WebSocket protocol directly — no browser, no Puppeteer — so it starts fast and sips RAM. Install the package as baileys (the old @whiskeysockets/baileys scope is superseded), and note that v7 requires Node 17+ and is published as ESM.

Authentication is by QR code or pairing code, and you persist the session with useMultiFileAuthState so you don't re-scan on every restart. The main entry point is makeWASocket. Here is a minimal echo-style bot that connects, prints a QR, and replies to incoming text:

TS
import makeWASocket, { useMultiFileAuthState } from 'baileys'
import qrcode from 'qrcode-terminal'

const { state, saveCreds } = await useMultiFileAuthState('auth')
const sock = makeWASocket({ auth: state })

sock.ev.on('creds.update', saveCreds)
sock.ev.on('connection.update', ({ qr }) => {
  if (qr) qrcode.generate(qr, { small: true }) // scan from WhatsApp > Linked devices
})

sock.ev.on('messages.upsert', async ({ messages }) => {
  const msg = messages[0]
  if (!msg.message || msg.key.fromMe) return
  const text = msg.message.conversation || msg.message.extendedTextMessage?.text
  await sock.sendMessage(msg.key.remoteJid!, { text: `You said: ${text ?? ''}` })
})

whatsapp-web.js follows the same shape conceptually — you create a Client, listen for a 'qr' event, then a 'message' event, and call message.reply(). The difference is footprint: it boots a full Chromium via Puppeteer per session, so it is heavier but tracks the live web UI closely and tends to support new web features sooner. Choose it when you want that web-client parity and don't mind the browser overhead; choose Baileys when you want minimal resources.

Deploying your bot

The two paths deploy very differently. A Cloud API bot is just a stateless web service — your webhook handler — so it deploys like any HTTP app: a container or serverless function behind HTTPS on a platform such as Railway, Render, Fly.io, or a VPS. There is no session to keep alive; Meta holds the WhatsApp connection. Set your env vars (access token, app secret, verify token, phone number ID), point the webhook at your public HTTPS domain, and you are done.

An unofficial bot is stateful — it holds a live WhatsApp connection and an auth session that must survive restarts. That rules out most serverless platforms. Run it on a persistent host (a small VPS or a long-running container), and mount the auth folder on durable storage so a redeploy doesn't force a fresh QR scan. For whatsapp-web.js you also need the Chromium dependencies present in the image, which is why its Docker images are larger.

  • Cloud API: stateless web service, deploy anywhere with HTTPS, no session to persist.
  • Baileys: persistent host, durable volume for the auth/ folder, lightweight (no browser).
  • whatsapp-web.js: persistent host + Chromium in the image; size the container for a full browser.
  • Never commit tokens, app secrets, or the auth session to git — use environment variables and secret storage.

Compliance and ban risk — read before you ship

Be honest with yourself about which platform you chose. Baileys and whatsapp-web.js are both unofficial and reverse-engineered, and both say so. whatsapp-web.js's README states plainly that 'WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.' The Baileys maintainers say they do not condone practices that violate the Terms of Service and discourage stalkerware, bulk, or automated messaging. These are not theoretical warnings.

Real bans are documented. In the project's own issue tracker, users report the 'Your account has been disabled because you are using an unofficial WhatsApp' block (see the linked whatsapp-web.js ban thread in Sources). Those user reports describe bans landing after widely varying volumes — sometimes after only a handful of messages — though the exact threshold is anecdotal, not an officially published number. An unofficial bot puts your personal or business number at risk of permanent ban, and a banned number is hard to recover. If the number matters to you or your business, that risk should weigh heavily.

The Cloud API, by contrast, has no ToS ban risk because it is the sanctioned channel — at the cost of per-message pricing and stricter messaging rules: the 24-hour window, template approval, and tiered messaging limits that begin around 250 business-initiated conversations to unique customers per 24 hours and scale up with quality and verification. Note that since October 7, 2025 these messaging limits apply per Business Portfolio, not per individual phone number, so adding more numbers under the same portfolio does not stack the cap. Match the platform to the stakes: prototypes and personal tools can tolerate the unofficial route; anything customer-facing or revenue-bearing should be on the official API.

Which path should you choose?

The decision is mostly about stakes and scale. Use the WhatsApp Cloud API for anything production or commercial: it is compliant, scales cleanly, has a free development sandbox, and carries no ban risk. Reach for Baileys when you want a lightweight prototype or a personal project, you don't want a browser running, and you accept that the number could be banned. Pick whatsapp-web.js when you specifically need WhatsApp Web feature parity and don't mind the Chromium footprint — but understand it carries the same ban risk as Baileys.

Whichever you choose, the engineering fundamentals are the same: handle messages idempotently, keep secrets out of source control, respond to webhooks fast, and never send unsolicited bulk messages. The platform decision sets your ceiling on safety and scale; your behavior decides whether you stay within it.

Frequently asked questions

Clean your lists before your bot sends a thing

However you build your bot, only message numbers that are actually on WhatsApp. Our hosted API tells you if a number is registered and returns the public profile picture, display name, about text, and business flag from a single HTTP call — read-only, no QR scan, no ban risk.

Explore the WhatsApp Profile API
Sponsored

Need cheap numbers to create, verify or warm up WhatsApp accounts for testing? GrizzlySMS rents virtual numbers for WhatsApp verification from under $1.

Get a WhatsApp number

Sources & further reading

Related

More from the blog

Written by
Eduardo Airaudo

Developer and founder of the WhatsApp Profile API. Building WhatsApp tooling and APIs since 2022.

What Our Users Say

Real reviews from our satisfied customers

4.5/5 (170 reviews)