Installing
ERR_PNPM_IGNORED_BUILDS or “native module was never built”
pnpm 10 and later do not run the install scripts of dependencies until you decide which ones may run. pnpm 11 stops the install with ERR_PNPM_IGNORED_BUILDS; pnpm 10 installs without them, and the failure shows up later.
Decide once in pnpm-workspace.yaml (or run pnpm approve-builds) and reinstall:
onlyBuiltDependencies: [better-sqlite3]. If better-sqlite3 was skipped, SQLiteStore throws better-sqlite3 is installed but its native module was never built. npm runs every install script, so it needs none of this.
The process aborts with Assertion failed: (env) != nullptr
You are on Node.js 24.19 or later with better-sqlite3 11 or 12. A Node.js regression (nodejs/node#65446) can abort the process in RemoveEnvironmentCleanupHook when a prepared statement is garbage-collected. Install better-sqlite3 13, which is built on N-API and not affected:
“built for another Node.js version” / NODE_MODULE_VERSION
A native module (usually better-sqlite3) was compiled for the Node.js you had before an upgrade. Rebuild it with the Node.js that runs the bot:
ERR_REQUIRE_ESM or require is not defined
botmesh is ESM-only. Use import, and add "type": "module" to your package.json (or use .mjs files). With TypeScript, set "module": "nodenext".
pnpm add @botmesh/core installs an older build
Right after a publish, a plain install can still get the previous beta until latest is moved, and a project that mixes builds can fail on missing exports or peer ranges. Check the tags with pnpm view @botmesh/core dist-tags and install the newest beta with the tag, for example pnpm add @botmesh/core@beta @botmesh/baileys@beta. See Packages and versions.
WhatsApp Web (Baileys)
The connection closes with 428 before the QR appears
WhatsApp closes connections that use an outdated handshake with 428 Connection Terminated. Check two things:
@whiskeysockets/baileysresolves to7.0.0-rc14or newer in your lockfile (pnpm why @whiskeysockets/baileys).@botmesh/baileyspins a working version; an override or an old lockfile can bring back an older one.- You do not pass a custom
browsertuple. Leavebrowserundefined (or use a preset such as'macOS'): non-standard identities can be refused with428.
wss://web.whatsapp.com and, at startup, raw.githubusercontent.com (to read the current WhatsApp Web version).
The QR never shows up
Print it in the terminal or save it as a PNG, or handle theqr event yourself:
pairingPhoneNumber: '+57300…' and type the code from the qr event in WhatsApp → Linked devices.
The bot asks for a new QR, or stops reconnecting
The provider reports the reason on theconnection event:
The same person shows up with two ids, or a flow stops waiting for the answer
WhatsApp addresses people by phone number (PN,…@s.whatsapp.net) and by an opaque id (LID, …@lid), and the form can change between messages. @botmesh/baileys resolves this before the bot sees the message: ctx.sender and ctx.from use the PN whenever it is known, otherwise the LID, and the raw values are in ctx.senderInfo.platformData (phoneNumber, lid).
- Never key your data on
ctx.raw(the rawremoteJid): usectx.sender/ctx.from. - A person who only has a LID (users with a WhatsApp username) keeps a stable LID id;
platformData.phoneNumberisundefined. Do not assumectx.senderis a phone number. - If the PN is learned mid-conversation, the person switches to the PN id and a conversation started under the LID no longer receives their messages. Give flows a
.timeout()(ortimeoutonc.ask) so it closes itself, and store bothplatformData.phoneNumberandplatformData.lidif you keep per-person records. - Access lists match exact ids: add the full id (
573001234567@s.whatsapp.net, or the@lidid), not a bare number. Build ids withtoJid('+57 300 123 4567').
Messages fail with a rate-limit error, or replies are slow
The Baileys provider queues sends in a token bucket (5 per chat every 10 s and 50 in total per minute by default) to lower the risk of a ban. A send that waits longer thanrateLimit.maxWait (30 s) rejects with RateLimitedError. Spread bulk sends with bot.broadcast() (batches, pauses, retries) instead of a loop of bot.send(). See Rate limiting.
WhatsApp Cloud API, Messenger and Instagram
Webhook signature mismatch (403)
Every webhook answered with403 means the X-Hub-Signature-256 check failed. Meta signs the exact bytes it sends, so:
- Pass the raw body to
provider.ingest(body, headers):express.raw({ type: 'application/json' }), theBufferyou collect withnode:http, or the raw body of your framework. A body parsed byexpress.json()and re-serialized never matches (withappSecretset, an already-parsed object is answered with400). - Use the App Secret of the app that owns the webhook (App settings → Basic), not the access token or the verify token.
- Do not put a proxy that rewrites the body (compression, JSON pretty-printing) in front of the endpoint.
allowUnsignedWebhooks: true turns the check off.
Meta cannot verify the webhook URL
TheGET handshake compares hub.verify_token with the provider’s verifyToken. Answer with provider.verifyChallenge(req.query) and 403 when it returns null. The URL must be public HTTPS (use a tunnel such as cloudflared or ngrok during development).
OutsideSessionWindowError
More than 24 hours passed since the user’s last message, and Meta only accepts approved templates. Send a { kind: 'template', … } or let the provider’s sessionWindow reopen the conversation for you. See WhatsApp 24h window.
RecipientUnreachableError with a test number
A test number can only write to the numbers listed under API setup → “To” in the Meta dashboard (Graph code 131030). Add the recipient there.
”the access token is invalid or expired” / “check the configured phone number id”
Graph code190 is an invalid or expired access token: use a permanent system-user token with whatsapp_business_messaging. Code 100/33 means the phoneNumberId does not exist or the token cannot reach it: copy the phone number id from API setup (it is not the phone number).
Rate limits (130429, 131056, 80007…)
They arrive as RateLimitedError with retryAfterMs. A short wait on a ctx send is retried once (BotConfig.rateLimitRetry), and bot.broadcast() retries per chat (retries, default 3). For bulk traffic, lower concurrency and add delayBetween.
Telegram
409 Conflict and the bot stops receiving updates
Two processes poll the same token (or a webhook is set while you poll). The provider stops on purpose (sessionTakenOver). Stop the other instance, or remove the webhook with deleteWebhook before using long polling.
”check the bot token and baseUrl” (404)
The token is malformed (copy it again from @BotFather) orbaseUrl points at a wrong path.
Webhook answered with 401
The X-Telegram-Bot-Api-Secret-Token header does not match webhookSecret. Pass the same value as secret_token to setWebhook.
The bot itself
BotNotRunningError: bot is not running
You sent, edited or deleted a message before await bot.start() or after bot.stop(). Schedule proactive sends from bot.on('ready', …) or after start() resolves.
ConfigValidationError: Invalid config.<option>
An option of new Bot() has the wrong type or an invalid duration. The message names the option (err.field); durations take a string ('30s', '5m') or milliseconds.
Plugin "<name>" is already plugged on this bot
Each plugin name can be plugged once. Unplug it first with the function bot.plug() returned, or give every instance multiple: true in its spec.
bot.when('hi') does not match “hi there”
Text triggers compare by normalized equality (case, accents and extra spaces are ignored), not “contains”. Use { pattern: 'hi', match: 'contains' } or a RegExp. See Triggers.
The bot does not answer in groups
CheckgroupMode ('mentions' and 'commands' restrict what the bot answers in groups) and the trigger scope. Group conversations are per participant by default.
The built-in texts are in English
Timeouts, retries, slash commands and form prompts default to English. Setnew Bot({ lang: 'es' }) for Spanish, or register your own texts with bot.locale(). See Internationalization.
A .persistent() flow forgets where it was after a restart
The default MemoryStore lives in RAM. Use a durable store (@botmesh/sqlite-store, @botmesh/redis-store, or new MemoryStore({ file })). See Persistent store.
Still stuck?
- Turn on debug logs:
new Bot({ provider, logger: pino({ level: 'debug' }) }). Chat and sender ids are hashed in the bot’s log lines by default (logIds). - Check the FAQ and the package README of your channel.
- Open an issue with the error message, the package versions (
pnpm ls @botmesh/*) and your Node.js version. Never paste tokens or session files.