@whatsmeow-node/whatsmeow-node
v0.5.1
Published
TypeScript bindings for whatsmeow (Go WhatsApp library) via subprocess IPC
Maintainers
Readme
whatsmeow-node
TypeScript/Node.js bindings for whatsmeow, the Go WhatsApp Web multidevice API library.
Communicates with a precompiled Go binary over stdin/stdout JSON-line IPC. No CGo, no native addons, no WebSocket reimplementation -- just a subprocess.
0.x -- The binding API is stable, but we stay on 0.x because the upstream whatsmeow library is itself pre-1.0. See Versioning for details.
[!CAUTION] Disclaimer: This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or affiliates. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners.
Use of this library may violate WhatsApp's Terms of Service. WhatsApp does not allow unofficial clients or automated messaging on their platform. Your account may be banned. Use at your own risk.
Do not use this for spamming, stalkerware, bulk messaging, or any purpose that violates WhatsApp's Terms of Service. The maintainers do not condone such use and bear no liability for misuse.
Current upstream: whatsmeow 0.0.0-20260305
Documentation
Full docs and guides at nicastelo.github.io/whatsmeow-node — getting started, API reference, events, error codes, troubleshooting, and examples.
Install
npm install @whatsmeow-node/whatsmeow-nodeThe correct binary for your platform is installed automatically via optionalDependencies.
Supported platforms:
| OS | x64 | arm64 | musl (Alpine) | |---------|-----|-------|---------------| | macOS | Yes | Yes | - | | Linux | Yes | Yes | x64 only | | Windows | Yes | Yes | - |
Philosophy
whatsmeow-node is a binding, not a framework. The goal is to expose whatsmeow's API to Node.js as faithfully as possible -- a 1:1 mapping with no added abstractions, convenience wrappers, or opinion about how you should structure your app. A binding should bind, not opine.
- No magic -- Message payloads match the whatsmeow protobuf schema directly. At runtime, message-sending methods accept JSON objects that follow the same shape that whatsmeow's
waE2E.Messageproto serializes to. If you know whatsmeow, you know this library. - No sweeteners -- We don't invent shorthand like
sendText(jid, "hello")or auto-build reply context. You construct the proto-shaped object yourself, exactly as whatsmeow expects it. - Typed where possible, open where needed --
sendMessageis typed for common message shapes (text, extended text with replies). For any otherwaE2E.Messageshape, usesendRawMessage, which accepts anyRecord<string, unknown>-- image messages, sticker messages, location messages, and anything whatsmeow adds in the future. At runtime both methods pass your object through to whatsmeow unchanged; the difference is only in TypeScript type checking.
Why? whatsmeow is a great, battle-tested library that deliberately exposes low-level proto structs instead of inventing convenience abstractions. We follow the same philosophy -- our job is just to make whatsmeow accessible from Node.js, nothing more. This keeps whatsmeow-node in sync with upstream automatically -- if whatsmeow supports it, you can use it. One less abstraction layer to maintain.
Build on top of this. If you want a higher-level API, build it as a separate package that depends on whatsmeow-node:
- A
sendText(jid, text)helper that constructs{ conversation: text } - A
reply(jid, text, quotedMsg)helper that buildsextendedTextMessagewithcontextInfo - A message builder/fluent API for composing media messages
- A bot framework with command routing, middleware, session management
- Queue-based sending with rate limiting
All of these are better as userland packages that can evolve independently from the binding.
Why whatsmeow?
Several open-source libraries exist for the WhatsApp Web protocol. They fall into two categories:
Direct protocol (reimplement the WebSocket protocol):
| | whatsmeow | Baileys | |---|---|---| | Language | Go | Node.js | | Memory | ~10-20 MB | ~50 MB | | Maintainer | tulir (Mautrix bridges) | WhiskeySockets community |
Browser automation (drive WhatsApp Web via Puppeteer/Selenium): whatsapp-web.js, WPPConnect, OpenWA -- these require a headless browser (~200-500 MB) and are more fragile.
Higher-level platforms like Evolution API and WAHA wrap one or more of the above.
We chose whatsmeow because:
- Reliability -- Powers the Mautrix WhatsApp bridge (24/7 for thousands of users), wacli, and many other projects. Arguably the most battle-tested implementation.
- Resource efficiency -- A single Go binary uses far less memory than a Node.js or Puppeteer process.
- Protocol correctness -- Meticulous about protocol compliance, reducing the risk of bans.
- Stability -- Consistent maintainership. Baileys has gone through multiple forks and breaking changes; browser-based libraries depend on a full browser engine.
The tradeoff is the IPC layer between Node.js and Go, but the subprocess approach keeps things simple: no CGo, no native addons, no WebSocket reimplementation in JavaScript.
Huge thanks to @tulir and the whatsmeow contributors for building and maintaining such a solid foundation.
Quick Start
import { createClient } from "@whatsmeow-node/whatsmeow-node";
const client = createClient({ store: "session.db" });
// Listen for events
client.on("qr", ({ code }) => {
// Use any QR renderer — e.g. `npm install qrcode-terminal`
// import qrcode from "qrcode-terminal";
// qrcode.generate(code, { small: true });
console.log("QR code:", code);
});
client.on("connected", ({ jid }) => console.log("Connected as", jid));
client.on("message", ({ info, message }) => {
console.log(`${info.pushName}: ${message.conversation ?? JSON.stringify(message)}`);
});
// Initialize and connect
const { jid } = await client.init();
if (!jid) {
// Not paired yet — set up QR channel before connecting
await client.getQRChannel();
}
await client.connect();Connection Lifecycle
The connection flow follows a specific sequence:
init() → connect() → "connected" event → operational → "disconnected" → auto-reconnect → "connected"Normal startup:
const { jid } = await client.init(); // Opens store, returns JID if already paired
if (!jid) {
await client.getQRChannel(); // Set up QR pairing (first time only)
}
await client.connect(); // Starts connection (async, returns immediately)
// Wait for "connected" event before sending messagesKey events:
| Event | Meaning | Action |
|-------|---------|--------|
| connected | WhatsApp connection established | Safe to send messages |
| disconnected | Connection lost | Auto-reconnect is built-in, no action needed |
| logged_out | Session revoked (user unlinked device) | Must re-pair — delete store and start over |
| stream_error | Protocol error from WhatsApp | Usually followed by auto-reconnect |
| keep_alive_timeout | Keep-alive pings failing | Connection may be degraded |
| keep_alive_restored | Keep-alive recovered | Connection is healthy again |
Resilient connection pattern:
const client = createClient({ store: "session.db" });
client.on("connected", ({ jid }) => {
console.log(`Connected as ${jid}`);
});
client.on("disconnected", () => {
console.log("Disconnected — waiting for auto-reconnect...");
});
client.on("logged_out", ({ reason }) => {
console.error(`Logged out: ${reason}. Must re-pair.`);
process.exit(1);
});
const { jid } = await client.init();
if (!jid) {
await client.getQRChannel();
client.on("qr", ({ code }) => {
// Render QR code for pairing
});
}
await client.connect();Auto-reconnect is always enabled — whatsmeow handles reconnection internally. You only need to handle logged_out (session revoked, must re-pair).
Error Handling
All client methods throw on failure. Errors are typed for easy handling:
import {
WhatsmeowError, // Base class for all whatsmeow errors
TimeoutError, // IPC command timed out
ProcessExitedError // Go binary crashed or exited
} from "@whatsmeow-node/whatsmeow-node";
try {
await client.sendMessage(jid, { conversation: "hello" });
} catch (err) {
if (err instanceof WhatsmeowError) {
console.error(`WhatsApp error [${err.code}]: ${err.message}`);
}
}Common error codes:
| Code | Source | Meaning |
|------|--------|---------|
| ERR_TIMEOUT | TS | IPC command timed out (default: 30s) |
| ERR_PROCESS_EXITED | TS | Go binary crashed or exited |
| ERR_NOT_INIT | Go | init() not called yet |
| ERR_INVALID_ARGS | Go | Missing or invalid arguments |
| ERR_INVALID_JID | Go | Malformed JID string |
| ERR_SEND | Go | Message send failed |
| ERR_UPLOAD | Go | Media upload failed |
| ERR_UNKNOWN_CMD | Go | Unrecognized IPC command |
Pattern for handling specific errors:
try {
await client.sendMessage(jid, { conversation: "hello" });
} catch (err) {
if (err instanceof WhatsmeowError) {
switch (err.code) {
case "ERR_SEND":
console.error("Message failed:", err.message);
break;
case "ERR_NOT_INIT":
console.error("Forgot to call init()");
break;
default:
throw err;
}
}
}Sending Messages
Text message:
await client.sendMessage(jid, { conversation: "Hello!" });Reply to a message:
await client.sendMessage(jid, {
extendedTextMessage: {
text: "This is a reply",
contextInfo: {
stanzaId: originalMessageId,
participant: originalSenderJid,
quotedMessage: { conversation: "the original text" },
},
},
});Image, location, contact card (use sendRawMessage for any proto shape):
// Upload then send an image
const media = await client.uploadMedia(https://github.com/nicastelo/whatsmeow-node/blob/v0.5.1/path/to/photo.jpg", "image");
await client.sendRawMessage(jid, {
imageMessage: {
URL: media.URL,
directPath: media.directPath,
mediaKey: media.mediaKey,
fileEncSHA256: media.fileEncSHA256,
fileSHA256: media.fileSHA256,
fileLength: String(media.fileLength),
mimetype: "image/jpeg",
caption: "Check this out",
},
});
// Send a location
await client.sendRawMessage(jid, {
locationMessage: {
degreesLatitude: -34.9011,
degreesLongitude: -56.1645,
name: "Montevideo",
},
});
// Send a contact card
await client.sendRawMessage(jid, {
contactMessage: {
displayName: "John Doe",
vcard: "BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nTEL:+1234567890\nEND:VCARD",
},
});sendRawMessage accepts any Record<string, unknown> matching the whatsmeow waE2E.Message proto schema. The JSON shape uses protojson field names (camelCase).
API
createClient(options)
Returns a WhatsmeowClient instance.
| Option | Type | Default | Description |
|------------------|----------|-----------|------------------------------------------|
| store | string | required | SQLite path (session.db) or Postgres URL (postgres://...) |
| binaryPath | string | auto | Path to the Go binary (auto-resolved from platform package) |
| commandTimeout | number | 30000 | IPC command timeout in milliseconds |
Connection
init()-- Open store and create whatsmeow client. Returns{ jid }if already paired.getQRChannel()-- Set up QR pairing channel. Call beforeconnect(). QR codes arrive asqrevents.pairCode(phone)-- Pair via phone number (alternative to QR). Call afterconnect().connect()-- Connect to WhatsApp (client.Connect())disconnect()-- Disconnect from WhatsApp (client.Disconnect())logout()-- Log out and remove device from WhatsApp (client.Logout())isConnected()-- Check connection status (client.IsConnected())isLoggedIn()-- Check login status (client.IsLoggedIn())waitForConnection(timeoutMs?)-- Wait until connected and logged in, or timeoutclose()-- Kill the Go subprocess (for cleanup)
Messaging
sendMessage(jid, message)-- Send a typed message (conversation, extended text with replies)sendRawMessage(jid, message)-- Send anywaE2E.Message-shaped JSON (untyped escape hatch)sendReaction(chat, sender, id, reaction)-- React to a message (empty string to remove)editMessage(chat, id, message)-- Edit a previously sent messagerevokeMessage(chat, sender, id)-- Revoke/delete a messagemarkRead(ids, chat, sender?)-- Mark messages as read
Polls
sendPollCreation(jid, name, options, selectableCount)-- Create and send a pollsendPollVote(pollChat, pollSender, pollId, pollTimestamp, options)-- Vote on a poll
Media
downloadMedia(msg)-- Download media from a received messagedownloadAny(message)-- Download media from any message type (auto-detects the media field)downloadMediaWithPath(opts)-- Download media using direct path and keys (lower-level)uploadMedia(path, mediaType)-- Upload media for sending ("image"|"video"|"audio"|"document")
Media uses temp file paths instead of base64 to avoid bloating the IPC pipe. The Go binary writes downloaded media to a temp file and returns the path. Upload returns { URL, directPath, mediaKey, fileEncSHA256, fileSHA256, fileLength } for use in message protos.
Contacts & Users
isOnWhatsApp(phones)-- Check if phone numbers are on WhatsAppgetUserInfo(jids)-- Get user info (status, picture ID, verified name)getProfilePicture(jid)-- Get profile picture URLgetUserDevices(jids)-- Get all devices for given usersgetBusinessProfile(jid)-- Get business profile info (address, email, categories, profile options, business hours)setStatusMessage(message)-- Set your account's status message
Groups
createGroup(name, participants)-- Create a groupgetGroupInfo(jid)-- Get group metadatagetGroupInfoFromLink(code)-- Get group info from an invite link without joininggetGroupInfoFromInvite(jid, inviter, code, expiration)-- Get group info from a direct invitegetJoinedGroups()-- List all joined groupsgetGroupInviteLink(jid, reset?)-- Get/reset invite linkjoinGroupWithLink(code)-- Join a group via invite linkjoinGroupWithInvite(jid, inviter, code, expiration)-- Join a group via direct inviteleaveGroup(jid)-- Leave a groupsetGroupName(jid, name)-- Update group namesetGroupTopic(jid, topic, previousId?, newId?)-- Update group topic/description with optional topic IDssetGroupDescription(jid, description)-- Update group descriptionsetGroupPhoto(jid, path)-- Update group photosetGroupAnnounce(jid, announce)-- Toggle announcement modesetGroupLocked(jid, locked)-- Toggle group lockedupdateGroupParticipants(jid, participants, action)-- Add/remove/promote/demotegetGroupRequestParticipants(jid)-- Get pending join requestsupdateGroupRequestParticipants(jid, participants, action)-- Approve/reject join requestssetGroupMemberAddMode(jid, mode)-- Set who can add members ("admin_add"|"all_member_add")setGroupJoinApprovalMode(jid, enabled)-- Enable/disable join approval
Communities
linkGroup(parent, child)-- Link a child group to a parent communityunlinkGroup(parent, child)-- Unlink a child group from a communitygetSubGroups(jid)-- Get sub-groups of a communitygetLinkedGroupsParticipants(jid)-- Get participants across linked groups
Presence
sendPresence(presence)-- Set online/offline statussendChatPresence(jid, presence, media?)-- Set typing/recording indicatorsubscribePresence(jid)-- Subscribe to a contact's presence
Newsletters
getSubscribedNewsletters()-- List subscribed newslettersnewsletterSubscribeLiveUpdates(jid)-- Subscribe to live updatescreateNewsletter(name, description, picture?)-- Create a newsletter/channelgetNewsletterInfo(jid)-- Get newsletter metadatagetNewsletterInfoWithInvite(key)-- Get newsletter info from invite linkfollowNewsletter(jid)-- Follow a newsletterunfollowNewsletter(jid)-- Unfollow a newslettergetNewsletterMessages(jid, count, before?)-- Fetch newsletter messages (paginate backward from server ID)getNewsletterMessageUpdates(jid, count, opts?)-- Get message updates (since timestamp or after server ID)newsletterMarkViewed(jid, serverIds)-- Mark messages as viewednewsletterSendReaction(jid, serverId, reaction, messageId)-- React to a newsletter messagenewsletterToggleMute(jid, mute)-- Mute/unmute a newsletteracceptTOSNotice(noticeId, stage)-- Accept a Terms of Service notice (required for some newsletter flows)uploadNewsletter(path, mediaType)-- Upload media for newsletter messages
Privacy & Settings
getPrivacySettings()-- Get all privacy settingstryFetchPrivacySettings(ignoreCache?)-- Fetch privacy settings from cache or serversetPrivacySetting(name, value)-- Update a privacy settinggetStatusPrivacy()-- Get default status audience rulessetDefaultDisappearingTimer(seconds)-- Set default disappearing timer (0 to disable)setDisappearingTimer(jid, seconds)-- Set disappearing timer for a specific chat
Blocklist
getBlocklist()-- Get blocked contactsupdateBlocklist(jid, action)-- Block/unblock a contact ("block"|"unblock")
QR & Link Resolution
getContactQRLink(revoke?)-- Generate or revoke your contact QR linkresolveContactQRLink(code)-- Resolve a contact QR code to user inforesolveBusinessMessageLink(code)-- Resolve a business message link
Calls
rejectCall(from, callId)-- Reject an incoming call
Configuration
setPassive(passive)-- Set passive mode (don't receive messages)setForceActiveDeliveryReceipts(active)-- Force sending delivery receiptsresetConnection()-- Reset the WebSocket connection
Message Helpers
generateMessageID()-- Generate a unique message IDbuildMessageKey(chat, sender, id)-- Build a protobuf message keybuildUnavailableMessageRequest(chat, sender, id)-- Build a request for unavailable messagesbuildHistorySyncRequest(info, count)-- Build a history sync request messagesendPeerMessage(message)-- Send a message to your own devicessendMediaRetryReceipt(info, mediaKey)-- Request re-upload of media from the sender
Bots
getBotListV2()-- Get the list of available botsgetBotProfiles(bots)-- Get profiles for specific bots
App State
fetchAppState(name, fullSync?, onlyIfNotSynced?)-- Fetch app state from the servermarkNotDirty(cleanType, timestamp)-- Mark an app state patch as not dirty
Decrypt / Encrypt
decryptComment(info, message)-- Decrypt a comment messagedecryptPollVote(info, message)-- Decrypt a poll vote messagedecryptReaction(info, message)-- Decrypt a reaction messagedecryptSecretEncryptedMessage(info, message)-- Decrypt a secret encrypted messageencryptComment(info, message)-- Encrypt a comment for a messageencryptPollVote(info, vote)-- Encrypt a poll voteencryptReaction(info, reaction)-- Encrypt a reaction
Web Message Parsing
parseWebMessage(chatJid, webMsg)-- Parse a WebMessageInfo (from history sync) into a message event
Generic
call(method, args)-- Send any command to the Go binary (escape hatch)
Events
All whatsmeow events are forwarded as typed events:
client.on("message", ({ info, message }) => { /* ... */ });
client.on("message:receipt", ({ type, chat, sender, ids }) => { /* ... */ });
client.on("connected", ({ jid }) => { /* ... */ });
client.on("disconnected", () => { /* ... */ });
client.on("qr", ({ code }) => { /* ... */ });
client.on("call:offer", ({ from, callId }) => { /* ... */ });
// ... and more (see src/types.ts for full list)Database
The store option accepts:
- SQLite:
session.dbor./data/wa.db-- Creates a local database file. Recommended for single-process usage. Plain paths are auto-prefixed withfile:(you can also passfile:session.db?_pragma=...for explicit SQLite URI parameters). - PostgreSQL:
postgres://user:pass@host/db-- For multi-instance deployments or serverless.
SQLite is configured automatically with WAL mode, foreign keys, and busy timeout for reliable concurrent access during WhatsApp's initial sync.
Usage with Next.js
Next.js (Turbopack/Webpack) bundles server code by default and will try to parse the Go binary as JavaScript. Add all @whatsmeow-node packages to serverExternalPackages in your next.config.ts:
const nextConfig: NextConfig = {
serverExternalPackages: [
"@whatsmeow-node/whatsmeow-node",
"@whatsmeow-node/darwin-arm64",
"@whatsmeow-node/darwin-x64",
"@whatsmeow-node/linux-arm64",
"@whatsmeow-node/linux-x64",
"@whatsmeow-node/linux-x64-musl",
"@whatsmeow-node/win32-arm64",
"@whatsmeow-node/win32-x64",
],
};Only your deployment platform's package will be installed (npm resolves by os/cpu), but listing all of them ensures it works in any environment.
Rate Limiting
WhatsApp enforces rate limits that can result in temporary bans if exceeded. There are no officially published limits, but the community has observed these approximate thresholds:
- Messages: ~50-80 messages per minute for individual chats, lower for new/unverified numbers
- Group operations: Creating groups, adding participants, and modifying settings are more tightly limited
- Media uploads: Slower rate limit than text messages; large files count more heavily
- Contact checks (
isOnWhatsApp): ~50 numbers per request, batched automatically by whatsmeow - Newsletter operations: Lower limits than regular messaging
Safe sending pattern:
async function sendWithBackoff(client: WhatsmeowClient, messages: Array<{ jid: string; text: string }>) {
for (const { jid, text } of messages) {
try {
await client.sendMessage(jid, { conversation: text });
} catch (err) {
if (err instanceof WhatsmeowError && err.code === "ERR_SEND") {
// Back off and retry
await new Promise((r) => setTimeout(r, 5000));
await client.sendMessage(jid, { conversation: text });
} else {
throw err;
}
}
// Space out messages: 1-3 seconds between sends
await new Promise((r) => setTimeout(r, 1000 + Math.random() * 2000));
}
}General guidance:
- Space out messages (1-3 seconds between sends)
- Avoid bulk operations on new/freshly paired numbers
- Handle
temporary_banevents — they include an expiry time - Monitor
stream_errorandkeep_alive_timeoutevents as early warning signs - Use
sendPresence("available")before sending to simulate normal client behavior
Differences from whatsmeow
The API maps closely to whatsmeow's Go API. Most methods have a 1:1 TypeScript equivalent. Key differences:
- Session storage is internal -- The Go binary manages the whatsmeow
store.Deviceinternally. You can't implement a custom store from TypeScript; you choose SQLite or Postgres via the connection string. - Messages are JSON, not protobuf -- You send/receive JSON objects that map to
waE2E.Messageprotobuf fields via protojson. The JSON shape matches the proto schema directly. - Auto-reconnect is enabled -- whatsmeow's built-in reconnection is always on. You see
disconnected+connectedevents but don't manage reconnect logic. - One client per process -- Each
createClient()spawns one Go binary. For multiple accounts, create multiple clients. - Network configuration not yet exposed --
SetProxy,SetMediaHTTPClient, etc. are not available. The Go binary uses default networking.
For the full comparison including what's not yet implemented, see INTERNALS.md.
Versioning
whatsmeow-node stays on 0.x because the upstream whatsmeow library is itself pre-1.0 (0.0.0-YYYYMMDD). Declaring 1.0 on top of an unstable upstream would be misleading. This is not a maturity signal — the binding layer, IPC contract, and TypeScript API are stable. The 0. prefix reflects upstream reality.
We use 0.MAJOR.MINOR semantics: the minor position acts as the major version, and patch acts as minor.
| What changed | Version bump |
|---|---|
| Breaking API or IPC change | Minor (0.5.0 → 0.6.0) |
| whatsmeow upstream bump (breaking) | Minor |
| New methods, features, upstream bump (non-breaking) | Patch (0.5.0 → 0.5.1) |
| Bug fix | Patch |
The exact whatsmeow commit is tracked in package.json as whatsmeowVersion.
Building from Source
Requirements: Go 1.25+, Node.js 18+
# Build the Go binary
cd cmd/whatsmeow-node
go build -o ../../whatsmeow-node .
# Build the TypeScript package
cd ../../ts
npm install
npm run build
# Run the pairing example
npx tsx examples/pair.tsSee CONTRIBUTING.md for the full development setup.
Testing
Unit tests (mock-based, no WhatsApp connection):
cd ts && npm testE2E tests (requires a paired WhatsApp session):
# Build the Go binary first
go build -o whatsmeow-node ./cmd/whatsmeow-node
# Run E2E tests against a session
E2E_SESSION_DB=./ts/session.db npm run test:e2eE2E tests run read-only operations (privacy settings, blocklist, groups, contacts, newsletters, presence) to minimize ban risk. They run nightly in CI against an encrypted session stored in the repo.
To set up E2E for CI:
- Pair a session:
cd ts && npx tsx examples/pair.ts - Encrypt and commit:
./scripts/export-session.sh ts/session.db - Set the secret:
gh secret set E2E_SESSION_KEY --body '<passphrase>'
Acknowledgments
This project is entirely built on whatsmeow by @tulir and contributors. All the hard work of protocol implementation, encryption, and WhatsApp compliance happens in whatsmeow — we just bridge it to Node.js.
whatsmeow-node is an independent project. It is not affiliated with, endorsed by, or connected to whatsmeow or its maintainers in any way.
If you find this project useful and want to support it financially, please sponsor whatsmeow's maintainer instead — without whatsmeow, this project wouldn't exist: github.com/sponsors/tulir
whatsmeow Resources
- GitHub · Go Docs · Matrix Chat · Protocol Q&A
