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
| Need | Backend |
|---|---|
| Production, real users, B2C | whatsapp-bot (Meta Cloud) |
| Personal phone, dev, prototyping | this example (Web bridge) |
| Group messages with WhatsApp Web reach | this example (Cloud doesn't deliver groups by default) |
| Zero infrastructure (no webhook URL, no public host) | this example |
| Multi-device support, status receipts, robust uptime | Cloud |
Prerequisites
- Node 22.12+
- A WhatsApp account on a phone you control (you'll scan a QR code from it).
- Disk space ~150 MB for Chromium + Puppeteer (
whatsapp-web.js's peer deps). - 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 installpnpm install will pull in whatsapp-web.js (which transitively installs Puppeteer + Chromium). First install can take 1-2 minutes.
Run
pnpm run runOn 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 installagain. 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-sandboxto 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.sendSame 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
/**
* 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