Theo Docs
TheoKitTheoKit-SDKTheoUITheo PaaS
Cookbook

whatsapp-web-bot

WhatsApp bot using the **unofficial** `whatsapp-web.js` subprocess bridge backend (ADR D305).

whatsapp-web-bot

WhatsApp bot using the unofficial whatsapp-web.js subprocess bridge backend (ADR D305).

[!CAUTION]

Account ban risk

WhatsApp does not officially support automation of personal accounts. Using whatsapp-web.js (or any other WhatsApp Web bridge: Baileys, etc.) violates the WhatsApp Terms of Service and can result in permanent account suspension.

Use this backend for:

  • Local development and prototyping
  • Personal automation on a throwaway number

Do NOT use for:

  • Production bots serving real users (use the Cloud backend for that — Meta-sanctioned, sandbox tier free)
  • Anything that requires account stability

When to use this vs the Cloud backend

NeedBackend
Production, real users, B2Cwhatsapp-bot (Meta Cloud)
Personal phone, dev, prototypingthis example (Web bridge)
Group messages with WhatsApp Web reachthis example (Cloud doesn't deliver groups by default)
Zero infrastructure (no webhook URL, no public host)this example
Multi-device support, status receipts, robust uptimeCloud

Prerequisites

  1. Node 22.12+
  2. A WhatsApp account on a phone you control (you'll scan a QR code from it).
  3. Disk space ~150 MB for Chromium + Puppeteer (whatsapp-web.js's peer deps).
  4. OpenRouter (or any provider) API key for the agent.

Setup

cp .env.example .env
# edit .env: set OPENROUTER_API_KEY (and optionally WHATSAPP_SESSION_ID, WHATSAPP_BOT_PHONE)
pnpm install

pnpm install will pull in whatsapp-web.js (which transitively installs Puppeteer + Chromium). First install can take 1-2 minutes.

Run

pnpm run run

On first run, the bridge subprocess prints a QR code to stderr. Open WhatsApp on your phone → ⋮ Menu → Linked devices → Link a device → scan the QR.

After successful pair:

[whatsapp-web-bot] connected (session: my-bot). Send a WhatsApp message to test.

Now send a WhatsApp message to your own number from another contact and watch the bot reply.

Session persistence

whatsapp-web.js saves credentials in ./.wwebjs_auth/session-<SESSION_ID>/. Subsequent runs use the same session — no QR rescan needed unless you log out from the phone.

To rotate sessions, change WHATSAPP_SESSION_ID in .env (will require a fresh QR scan).

Group conversations

By default, groups are dropped (D309 + EC-7: require @mention). Set WHATSAPP_BOT_PHONE in .env to enable the mention filter (digits-only normalizer matches @5511..., @+5511..., @99999-9999, etc.).

Lifecycle

The bridge runs as a child process under the bot. PID file at $THEOKIT_HOME/whatsapp-bridge-<SESSION_ID>.pid (default: ~/.theokit/). On restart, the bot detects stale PIDs and kills them — but only if the cmdline matches whatsapp-web-bridge (EC-5 safety guard).

Press Ctrl+C to shut down cleanly. The bridge sends SIGTERM → 3s grace → SIGKILL.

Troubleshooting

  • "WhatsAppConnectTimeoutError" → you didn't scan the QR within 120s. Re-run.
  • "whatsapp-web.js not installed" → run pnpm install again. The bridge subprocess detects the peer dep at boot.
  • Bot stops responding mid-session → WhatsApp likely flagged it. Check phone for "device disconnected". You may have triggered the ban risk.
  • Puppeteer "No usable sandbox" → on Linux without sandbox, the bundled bridge passes --no-sandbox to Chromium. Already handled.
  • QR code regenerates repeatedly → check .wwebjs_auth/session-<id>/ directory is writable.

Architecture (vs Cloud)

Cloud backend                    Web bridge backend
─────────────                    ──────────────────
Meta webhook → POST URL          (no inbound URL — subprocess holds session)
   │                                 │
   ▼                                 ▼
Express server                   Bridge subprocess (whatsapp-web.js)
   │                                 │  stdio JSON-lines IPC
   ▼                                 ▼
WhatsAppCloudBackend             WhatsAppWebBackend
   │                                 │
   ▼                                 ▼
WhatsAppAdapter ←───── identical adapter surface (D303) ────→ WhatsAppAdapter
   │                                 │
   ▼                                 ▼
Agent.create / agent.send        Agent.create / agent.send

Same WhatsAppAdapter surface — your application code is identical whichever backend you pick.

Companion: official Cloud backend

For production / B2C: see examples/whatsapp-bot/. Same agent loop, different backend instantiation.

Code

run.ts
/**
 * WhatsApp bot example — UNOFFICIAL whatsapp-web.js subprocess bridge backend.
 *
 * Use this for personal accounts where Meta Business verification isn't an
 * option (dev, prototype, side projects). See README for risk disclosure
 * (account ban risk — WhatsApp doesn't officially support automation).
 *
 * No webhook server. The bridge subprocess maintains the WhatsApp Web
 * session in headless Chromium; you scan a QR code once and the session
 * persists in LocalAuth (`./wwebjs_auth/`).
 */

import {
  WhatsAppAdapter,
  WhatsAppWebBackend,
  type WhatsAppMessageEvent,
} from "@theokit/gateway-whatsapp";
import { Agent } from "@theokit/sdk";

const PROVIDER_KEY = required("OPENROUTER_API_KEY");
const SESSION_ID = process.env.WHATSAPP_SESSION_ID ?? "my-bot";
const BOT_PHONE = process.env.WHATSAPP_BOT_PHONE;

function required(name: string): string {
  const v = process.env[name];
  if (v === undefined || v.length === 0) {
    console.error(`[whatsapp-web-bot] missing env var: ${name}`);
    process.exit(1);
  }
  return v;
}

const backend = new WhatsAppWebBackend({ sessionId: SESSION_ID });
const adapter = new WhatsAppAdapter(backend, {
  // Group mention filter only kicks in when botPhoneId is set.
  ...(BOT_PHONE !== undefined && BOT_PHONE.length > 0 ? { botPhoneId: BOT_PHONE } : {}),
  // Without BOT_PHONE, groups are dropped (silent default — see EC-7 / D309).
});

adapter.onInbound(async (event) => {
  if (event.platform !== "whatsapp") return;
  const wa = event as WhatsAppMessageEvent;
  console.log(`[whatsapp-web-bot] in from=${wa.sender.id} text=${wa.text.slice(0, 60)}`);

  const agent = await Agent.create({
    apiKey: PROVIDER_KEY,
    model: { id: "openai/gpt-4o-mini" },
    name: `whatsapp-web-${wa.channel.id}`,
    systemPrompt: "You are a concise WhatsApp assistant. Reply in one short paragraph.",
  });
  try {
    const run = await agent.send(wa.text);
    const result = await run.wait();
    await adapter.sendMessage({
      channel: wa.channel,
      text: result.result ?? "(no reply)",
    });
  } finally {
    await agent.dispose();
  }
});

adapter.onStatusReceipt(async (receipt) => {
  console.log(`[whatsapp-web-bot] status wamid=${receipt.wamid} → ${receipt.status}`);
});

console.log("[whatsapp-web-bot] starting — scan the QR code printed below on first run.");
console.log("[whatsapp-web-bot] (the bridge prints the QR to stderr — open WhatsApp on your phone → Linked devices)");

try {
  await adapter.connect();
  console.log("[whatsapp-web-bot] connected (session: " + SESSION_ID + "). Send a WhatsApp message to test.");
} catch (err) {
  console.error("[whatsapp-web-bot] connect failed:", err instanceof Error ? err.message : err);
  process.exit(1);
}

process.on("SIGINT", async () => {
  console.log("\n[whatsapp-web-bot] shutting down");
  await adapter.disconnect();
  process.exit(0);
});

Run

cd examples/whatsapp-web-bot
cp .env.example .env  # fill in keys
pnpm install
pnpm run run

Repository

examples/whatsapp-web-bot

On this page