whalibmob
v5.5.51
Published
WhatsApp library for interaction with WhatsApp Mobile API no web
Maintainers
Readme
[!IMPORTANT] new repository whalibmob It will be maintained at the https://github.com/Kunboruto50/whalibmob.git Since I lost the Kunboruto20 account, whalibmob will be maintained and rewritten by me soon because I know that many functions in whalibmob no longer work after the WhatsApp mobile protocol change, these days I will take care of whalibmob, all good friends Fun enjoyable for all c
CONTACT ME ON TELEGRAM IF YOU WANT TO WORK WITH ME AND IF YOU HAVE PROBLEM WITH WHALIBMOB : @borutokun240
TELEGRAM NEW WHALIBMOB CHANNEL JOIN HERE https://t.me/+jWzq-I9o0Xc1Mzc8
[!CAUTION] Use a dedicated phone number with this library. Connecting with a number that is already active on a real device will cause WhatsApp to log that device out.
[!CAUTION] Whalibmob now It needs to be rewritten because WhatsApp mobile and has changed the protocol lately and now whalibmob is in testing and some updates by Me Any pull request is accepted.
[!IMPORTANT] If you like what I do and want to support me I can leave you here my Crypto usdc address for any donation and support any Small donation is accepted because the WhatsApp protocol changes very often : "0x8AD64F47a715eC24DeF193FBb9aC64d4E857f0f3"
Usdc ethereum network.
[!IMPORTANT] 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" and related names are registered trademarks of their respective owners. Use at your own discretion.
- whalibmob does not require a browser, Selenium, or any other external runtime — it communicates directly with WhatsApp using a TCP socket and the Noise Protocol handshake.
- The library operates as a real iOS mobile device, not as WhatsApp Web. It uses the Mobile API endpoint, which behaves differently from the Web API.
- Signal Protocol encryption is fully inlined in pure JavaScript — no native binaries, no node-gyp, runs anywhere Node.js runs.
Install
npm install whalibmobInstall the CLI globally:
npm install -g whalibmobIndex
- CLI — Getting Started
- CLI — Interactive Shell Commands
- Messaging Commands
- Presence Commands
- Profile Commands
- Contact Commands
- Chat Management Commands
- Group Commands
- Create a Group
- Leave a Group
- Add / Remove Participants
- Promote / Demote Admins
- Change Group Name
- Change Group Description
- Change Group Picture
- Get Invite Link
- Revoke Invite Link
- Join a Group by Invite Code
- Query Group Invite Info
- List All Groups
- Query Group Metadata
- List Group Participants
- Pending Join Requests
- Approve / Reject Join Requests
- Group Settings
- Community Commands
- Newsletter / Channel Commands
- Business Profile Command
- Registration Commands (in-shell)
- Connection Commands (in-shell)
- Full Command Reference Table
- Library API
- WhatsApp IDs
- Transport
- Media Encryption
- Device Emulation
Library API
Connecting Account
Register a New Number
Registration is a one-time process. You need a phone number that can receive an SMS or voice call.
Step 1 — request a verification code
const {
createNewStore, saveStore, requestSmsCode
} = require('whalibmob')
const path = require('path')
const fs = require('fs')
const phone = '919634847671' // country code + number, no '+'
const sessDir = path.join(process.env.HOME, '.waSession')
const sessFile = path.join(sessDir, phone + '.json')
fs.mkdirSync(sessDir, { recursive: true })
const store = createNewStore(phone)
saveStore(store, sessFile)
await requestSmsCode(store, 'sms') // 'sms' | 'voice' | 'wa_old'Step 2 — verify the code
const { loadStore, saveStore, verifyCode } = require('whalibmob')
const store = loadStore(sessFile)
const result = await verifyCode(store, '123456')
if (result.status === 'ok') {
saveStore(result.store, sessFile)
console.log('registered')
}Connect
const { WhalibmobClient } = require('whalibmob')
const path = require('path')
const client = new WhalibmobClient({
sessionDir: path.join(process.env.HOME, '.waSession')
})
client.on('connected', () => {
console.log('connected')
})
await client.init('919634847671')Saving & Restoring Sessions
Sessions are automatically persisted to disk as JSON files under the sessionDir you provide. The file is named <phone>.json. On the next client.init() call the session is restored and no re-registration is needed.
const client = new WhalibmobClient({
sessionDir: path.join(process.env.HOME, '.waSession')
})
// no need to register again — just connect
await client.init('919634847671')[!NOTE] Each phone number uses its own session file. The library handles Signal Protocol key persistence automatically.
Signal Store Utilities
auth-utils is a collection of optional helpers for power users who manage their own SignalStore instances directly (e.g. custom storage backends, multi-account servers).
const {
makeCacheableSignalKeyStore,
addTransactionCapability,
assertMeId,
initAuthCreds
} = require('whalibmob')makeCacheableSignalKeyStore
Wraps a SignalStore with an in-memory NodeCache layer (5-minute TTL). All get calls for sessions, preKeys, signedPreKeys, and identities are served from cache on subsequent accesses. Writes invalidate the cache automatically.
useClones is set to false so that SessionRecord objects — which carry internal state and methods — are returned by reference and never deep-cloned.
The wrapper also forwards transaction() and isInTransaction() calls to the underlying store when present, making it safe to stack with addTransactionCapability.
const { SignalStore } = require('whalibmob')
const { makeCacheableSignalKeyStore } = require('whalibmob')
const store = new SignalStore(/* ... */)
const cached = makeCacheableSignalKeyStore(store)
// reads hit cache after first access
const session = await cached.getSession('[email protected]:0')When to use: whenever your SignalStore is backed by a remote or disk-based store (database, Redis, file system) and you want to reduce repeated lookups for sessions that haven't changed between sends.
addTransactionCapability
Wraps a SignalStore with batched-write (transaction) semantics. During a transaction all writes are buffered in memory; they are flushed to the underlying store atomically when commit() is called at the end of the transaction.
Uses AsyncLocalStorage to propagate transaction context across async call chains, and a per-key-type Mutex with reference-counting to serialize concurrent writers safely.
const { addTransactionCapability, makeCacheableSignalKeyStore } = require('whalibmob')
// recommended: cache first, then transactions on top
const base = new SignalStore(/* ... */)
const cached = makeCacheableSignalKeyStore(base)
const txnStore = addTransactionCapability(cached)
// inside a send flow
await txnStore.transaction(async () => {
// all writes are buffered
await txnStore.setSession('[email protected]:0', sessionRecord)
await txnStore.setPreKey(1, preKeyPair)
// commit is called automatically at the end of the transaction callback
})Stacking order matters: put makeCacheableSignalKeyStore below addTransactionCapability so that the cache always sees the committed state.
When to use: for high-throughput servers that send to many recipients concurrently and need to batch Signal key writes into a single atomic flush per message.
assertMeId
Validates that a store object has a registered phone number and returns the canonical @s.whatsapp.net JID. Throws an Error if the store lacks a phoneNumber or has registered !== true.
const { assertMeId } = require('whalibmob')
const store = loadStore(sessFile)
try {
const jid = assertMeId(store)
// jid === '[email protected]'
console.log('account JID:', jid)
} catch (err) {
console.error('store is not registered:', err.message)
}When to use: as a guard before calling client.init() to give a clear error message when a corrupted or unregistered session file is accidentally loaded.
initAuthCreds
Creates a fresh credential store for the given phone number. Functionally equivalent to createNewStore but also initialises the Baileys-compatible extra fields that the library expects for account sync: nextPreKeyId, firstUnuploadedPreKeyId, accountSyncCounter, accountSettings, and advSecretKey.
const { initAuthCreds, saveStore } = require('whalibmob')
const path = require('path')
const fs = require('fs')
const phone = '919634847671'
const sessDir = path.join(process.env.HOME, '.waSession')
const sessFile = path.join(sessDir, phone + '.json')
fs.mkdirSync(sessDir, { recursive: true })
const store = initAuthCreds(phone)
saveStore(store, sessFile)This is the function used internally by the CLI for all new session creation. Prefer it over createNewStore for forward compatibility.
[!NOTE]
initAuthCredsandcreateNewStoreproduce equivalent stores for all current library operations. The additional fields frominitAuthCredsare there for future-proofing and interoperability.
Recommended Stacking Pattern
For a production multi-account server:
const {
SignalStore,
makeCacheableSignalKeyStore,
addTransactionCapability,
initAuthCreds,
saveStore,
loadStore
} = require('whalibmob')
// 1. load or create the credential store
let store = loadStore(sessFile) || initAuthCreds(phone)
// 2. build the layered Signal key store
const signalStore = new SignalStore(store)
const cachedStore = makeCacheableSignalKeyStore(signalStore)
const txnStore = addTransactionCapability(cachedStore)
// 3. pass to the client (advanced usage — most users should use WhalibmobClient directly)For standard usage, WhalibmobClient handles all of this internally. These helpers are for advanced scenarios where you need direct control over Signal key storage.
Handling Events
whalibmob uses the EventEmitter syntax for events.
Example to Start
const { WhalibmobClient } = require('whalibmob')
const path = require('path')
async function connect() {
const client = new WhalibmobClient({
sessionDir: path.join(process.env.HOME, '.waSession')
})
client.on('connected', async () => {
console.log('connected')
await client.sendText('[email protected]', 'Hello!')
})
client.on('disconnected', () => {
console.log('disconnected — reconnecting...')
setTimeout(() => connect(), 3000)
})
client.on('message', msg => {
const d = msg.decoded
if (d && d.type === 'text') console.log('message from', msg.from, d.text)
})
client.on('auth_failure', ({ reason }) => {
console.error('session revoked:', reason)
// re-register the number
})
await client.init('919634847671')
}
connect()All Events
| Event | Payload | Description |
|---|---|---|
| connected | — | Session authenticated and ready |
| disconnected | — | Connection closed |
| reconnecting | { attempt, delay } | Lost connection, will retry |
| reconnected | — | Connection restored |
| auth_failure | { reason } | Session revoked or banned |
| message | message object | Incoming message received |
| receipt | { type, id, from } | Delivery / read / played receipt |
| presence | { from, available } | Contact came online or went offline |
| group_update | { type, groupJid, actor, participants, subject, timestamp } | Member added / removed / promoted / demoted, subject or settings changed |
| notification | node object | Group or contact update notification |
| call | { from } | Incoming call event |
| chat_read | { jid, read } | Chat marked read (read: true) or unread (read: false) |
| chat_muted | { jid, muted, until } | Chat muted or unmuted; until is epoch ms (−1 = indefinite) |
| chat_pinned | { jid, pinned } | Chat pinned or unpinned |
| chat_archived | { jid, archived } | Chat archived or unarchived |
| message_starred | { msgId, chatJid, starred } | Message starred or unstarred |
| stream_error | { reason } | Server sent a fatal stream error |
| decrypt_error | { id, from, participant, err } | Failed to decrypt an incoming message |
| session_refresh | { node } | Late re-authentication success; Signal session refreshed |
| close | — | Underlying TCP socket closed |
| error | Error | Unhandled transport error |
The message object contains:
{
id: string, // unique message ID
from: string, // sender JID — may be a LID (e.g. '[email protected]')
participant: string, // group member JID (groups only; equals from for DMs)
ts: number, // Unix timestamp (seconds)
node: object, // raw XML node — node.attrs.sender_pn holds the real phone JID
decoded: object, // structured payload — shape depends on message type (see below)
}[!NOTE] WhatsApp Multi-Device uses LID JIDs internally. The
fromfield may be a LID like[email protected]rather than the real phone number. To get the actual phone number JID always readmsg.node.attrs.sender_pn:const spn = msg.node.attrs.sender_pn // { user: '919634847671', server: 's.whatsapp.net' } const phoneJid = spn.user + '@s.whatsapp.net' // '[email protected]'
The decoded object shape per message type:
// Text
{ type: 'text', text: string }
// Image
{ type: 'image', caption: string, url: string, mimetype: string, mediaKey: Buffer, directPath: string }
// Video
{ type: 'video', caption: string, url: string, mimetype: string, mediaKey: Buffer, directPath: string }
// Audio (music file)
{ type: 'audio', url: string, mimetype: string, mediaKey: Buffer, directPath: string }
// Voice note (push-to-talk)
{ type: 'voice', url: string, mimetype: string, mediaKey: Buffer, directPath: string }
// Document
{ type: 'document', fileName: string, url: string, mimetype: string, mediaKey: Buffer, directPath: string }
// Sticker
{ type: 'sticker', url: string, mimetype: string, mediaKey: Buffer, directPath: string }
// Reaction
{ type: 'reaction', emoji: string }
// Location
{ type: 'location', latitude: number, longitude: number, name: string, address: string, url: string }
// Contact (vCard)
{ type: 'contact', displayName: string, vcard: string }
// Protocol (revoke, ephemeral, etc.)
{ type: 'protocol', subtype: string }History Sync
How History Sync Works
When whalibmob connects, WhatsApp automatically sends the account's chat history to the client. The library handles the entire pipeline without any code from you — you only need to listen to the events if you want to use the data.
The full internal flow:
- WhatsApp server sends an encrypted
ProtocolMessage(type 6) containing aHistorySyncNotification. - The library decrypts it via Signal Protocol.
HistorySyncHandlerdownloads the encrypted blob from WhatsApp's CDN (mmg.whatsapp.net), or reads the inline payload if the server embedded it directly.- The blob is decrypted with AES-256-CBC using an HKDF key derived from
"WhatsApp History Keys". - The result is decompressed with zlib and decoded from protobuf (WAProto v2.3000.x — field numbers verified against the official proto definition).
- Chats, contacts, push names, LID↔PN mappings, and tcTokens are merged into the disk store.
- tcTokens from history are seeded into
TcTokenStorein memory so the first outbound DM after reconnect already carries a valid<tctoken>node (prevents error 463 on cold start). - The
history_syncevent fires with a summary of what was received.
History arrives in multiple chunks. Each chunk fires one history_sync event. The first chunk (sync type INITIAL_BOOTSTRAP) is usually the largest and carries the most recent conversations.
Listening to History Sync Events
const { WhalibmobClient } = require('whalibmob')
const path = require('path')
const client = new WhalibmobClient({
sessionDir: path.join(process.env.HOME, '.waSession')
})
// history_sync fires once per history chunk — may fire multiple times on first connect
client.on('history_sync', result => {
console.log('History sync chunk received:')
console.log(' type :', result.syncTypeName) // e.g. 'INITIAL_BOOTSTRAP', 'RECENT', 'FULL'
console.log(' progress:', result.progress) // 0-100 server-reported
console.log(' chunk :', result.chunkOrder)
console.log(' chats :', result.chats.length)
console.log(' contacts:', result.contacts.length)
console.log(' pushNames:', (result.pushNames || []).length)
})
// history_sync_error fires if a chunk fails to download or decrypt
client.on('history_sync_error', ({ err, notification }) => {
console.error('History sync failed:', err.message)
console.error(' syncType:', notification.syncType)
})
await client.init('919634847671')The result object shape emitted by history_sync:
{
syncType: number, // HistorySyncType enum value
syncTypeName: string, // 'INITIAL_BOOTSTRAP' | 'RECENT' | 'FULL' | 'PUSH_NAME' | 'NON_BLOCKING_DATA' | 'ON_DEMAND'
progress: number, // 0-100, server-reported progress
chunkOrder: number, // chunk sequence number
chats: [ // one entry per conversation in this chunk
{
id: string, // chat JID e.g. '[email protected]' or '[email protected]'
name: string, // display name (may be undefined for unknown contacts)
unreadCount: number,
lastMsgTimestamp: number, // Unix seconds
messageCount: number // number of messages in this chunk for this chat
}
],
contacts: [ // one entry per contact discovered in this chunk
{
id: string,
name: string,
username: string, // WhatsApp username if set
pnJid: string, // phone-number JID e.g. '[email protected]'
lidJid: string // LID JID e.g. '112345678901234@lid'
}
],
pushNames: Array, // push-name entries { id, pushname }
lidPnMappings: Array, // LID<->PN mapping entries { lidJid, pnJid }
merged: object // raw merged history store (see Reading the History Store below)
}Sync type values:
| syncTypeName | When it fires |
|---|---|
| INITIAL_BOOTSTRAP | First connect — most recent conversations |
| RECENT | Reconnect after a short offline period |
| FULL | Full historical sync (older messages) |
| PUSH_NAME | Contact name updates only |
| NON_BLOCKING_DATA | Background low-priority data |
| ON_DEMAND | Explicitly requested by the client |
Persistent Files Written to Disk
The library automatically writes these files to sessionDir per account. You do not need to create or manage them.
| File | Contents |
|---|---|
| <phone>.history.json | Chats, contacts, push names, LID↔PN mappings, tcTokens |
| <phone>.messages.json | Flat map of msgId → message metadata |
| <phone>.appStateKeys.json | App-state sync keys (used for app-state patch decryption) |
| <phone>.tctoken.json | Trusted-contact token store (tcToken per contact JID) |
Reading the History Store
After history sync completes you can read the on-disk files directly:
const fs = require('fs')
const path = require('path')
const sessDir = path.join(process.env.HOME, '.waSession')
const phone = '919634847671'
// ── Read chats ────────────────────────────────────────────────────────────────
const histPath = path.join(sessDir, phone + '.history.json')
const hist = JSON.parse(fs.readFileSync(histPath, 'utf8'))
// List all chats sorted by last message time
const chats = Object.values(hist.chats)
.sort((a, b) => (b.lastMsgTimestamp || 0) - (a.lastMsgTimestamp || 0))
for (const chat of chats.slice(0, 10)) {
console.log(chat.id, '|', chat.name || '(unknown)', '|', chat.unreadCount, 'unread')
}
// ── LID ↔ PN lookup ───────────────────────────────────────────────────────────
// Look up LID JID from phone number JID
const myLid = hist.pnLidMap['[email protected]']
console.log('LID:', myLid) // e.g. '112345678901234@lid'
// Reverse: phone number JID from LID
const myPn = hist.lidPnMap[myLid]
console.log('PN:', myPn)
// ── Read message metadata ─────────────────────────────────────────────────────
const msgPath = path.join(sessDir, phone + '.messages.json')
const msgs = JSON.parse(fs.readFileSync(msgPath, 'utf8'))
const msgList = Object.values(msgs).sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
console.log('Total messages indexed:', msgList.length)
console.log('Latest:', msgList[0])hist.chats schema per chat entry:
{
id: string, // JID
name: string,
displayName: string,
unreadCount: number,
lastMsgTimestamp: number, // Unix seconds
messageCount: number, // total messages indexed from history
ephemeralExpiry: number, // disappearing messages timer in seconds, if set
archived: boolean,
pinned: number, // pin sort order (0 = not pinned)
tcToken: string, // base64 — trusted-contact token (used internally by the library)
tcTokenTimestamp: number, // Unix seconds — when the token was issued by the server
tcTokenSenderTimestamp: number // Unix seconds — sender-side issuance timestamp for 7-day bucket dedup
}msgs schema per message entry:
{
id: string, // WhatsApp message ID
chatId: string, // JID of the conversation
fromMe: boolean,
fromJid: string, // sender JID
timestamp: number, // Unix seconds
pushName: string, // display name of sender at send time
status: number // 0=error 1=pending 2=server 3=delivered 4=read 5=played
}tcToken — Error 463 Defense
[!IMPORTANT] This section is informational. The entire tcToken lifecycle is fully automatic. You do not need to write any code for it.
WhatsApp counts every outbound DM sent without a <tctoken> node as an anonymous "reach-out" event. Once enough such events accumulate the server enforces a time-based Reach-out Time-lock and returns error 463 (NackCallerReachoutTimelocked), blocking all outbound messages and calls for a period.
whalibmob implements the full lifecycle to prevent this:
| Step | What the library does automatically |
|---|---|
| History seed | On every history sync chunk, tcToken bytes are extracted from each conversation in the protobuf and loaded into TcTokenStore in memory. The first send after reconnect already has a valid token ready — no 463 risk on cold start. |
| Attach on send | Before dispatching any DM, MessageSender looks up the token for the recipient JID, checks it has not expired (28-day rolling window), and pushes a <tctoken> child node into the message stanza. |
| Proactive issuance | After each successful DM send, the library fires a <iq type='set' xmlns='privacy'> requesting a fresh token for that JID from the server — once per 7-day bucket, deduplicated in-flight. |
| Incoming notification | When a contact starts a new conversation, WhatsApp pushes a <notification type='privacy_token'>. The library catches it in _handlePrivacyTokenNotification and stores the token immediately. |
| Identity change re-issue | When decrypting a pkmsg (new Signal session from peer), the library calls _reissueTcTokenAfterIdentityChange to re-issue the token for the new session. |
| Error 463 recovery | If a send fails with error 463, the library issues a fresh token, waits for the server response, and automatically retries the same message with the new token attached. |
| Expiry | Tokens use a 4-bucket rolling window (4 × 7 days = 28-day TTL). Expired tokens are cleared before the send and a fresh one is requested proactively. |
Token storage uses the LID JID of the contact (e.g. 112345678901234@lid) as the key — never the phone-number JID — matching WhatsApp's internal convention.
What Is Automatic vs What You Need to Do
Everything in the "Automatic" column requires zero code from you.
| Feature | Automatic | Notes |
|---|---|---|
| Download history blob from CDN | ✅ | |
| Decrypt history blob (AES-256-CBC + HKDF) | ✅ | |
| Decompress zlib | ✅ | |
| Decode protobuf (WAProto v2.3000.x) | ✅ | Field numbers verified against official proto |
| Persist chats / contacts / push names | ✅ | Written to <phone>.history.json |
| Persist message metadata | ✅ | Written to <phone>.messages.json |
| Persist app-state sync keys | ✅ | Written to <phone>.appStateKeys.json |
| Seed tcTokens into memory on connect | ✅ | Prevents error 463 on first send after reconnect |
| Attach tcToken to every outbound DM | ✅ | |
| Issue fresh tcTokens after each send | ✅ | Once per 7-day bucket per contact |
| Handle incoming privacy_token notifications | ✅ | |
| Re-issue tcToken after peer identity change | ✅ | |
| Recover from error 463 with automatic retry | ✅ | |
| Populate in-memory LID↔PN maps | ✅ | |
| Listen to history_sync event | 🔵 Optional | Only if your app needs to react to history data |
| Read <phone>.history.json | 🔵 Optional | Only if your app needs chat/contact data at rest |
| Read <phone>.messages.json | 🔵 Optional | Only if your app indexes messages |
Minimum working integration — history sync, tcTokens, and error-463 defense all active with zero extra code:
const { WhalibmobClient } = require('whalibmob')
const path = require('path')
const client = new WhalibmobClient({
sessionDir: path.join(process.env.HOME, '.waSession')
})
// History sync, tcToken seeding, error-463 defense, and all disk persistence
// happen automatically. Add the listeners below only if your app needs the data.
client.on('history_sync', result => {
// Optional — fires once per chunk (multiple times on first connect)
console.log('[', result.syncTypeName, ']',
result.chats.length, 'chats,',
result.contacts.length, 'contacts')
})
client.on('history_sync_error', ({ err }) => {
// Optional — log failures (library continues working even if a chunk fails)
console.error('History chunk failed:', err.message)
})
await client.init('919634847671')Receiving Media
When a media message arrives, msg.decoded contains a CDN url and a mediaKey.
The actual file is stored encrypted on WhatsApp's CDN and must be downloaded and decrypted.
Decryption uses two steps:
- HKDF-SHA256 expands
mediaKeyinto IV, cipher key, and MAC key. - AES-256-CBC decrypts the ciphertext; a 10-byte HMAC-SHA256 MAC is verified first.
const crypto = require('crypto')
const https = require('https')
const http = require('http')
const fs = require('fs')
const path = require('path')
// HKDF info strings per media type
const MEDIA_HKDF_INFO = {
image: 'WhatsApp Image Keys',
video: 'WhatsApp Video Keys',
audio: 'WhatsApp Audio Keys',
voice: 'WhatsApp Audio Keys',
document: 'WhatsApp Document Keys',
sticker: 'WhatsApp Image Keys',
}
function deriveMediaKeys(mediaKey, mediaType) {
const info = Buffer.from(MEDIA_HKDF_INFO[mediaType] || 'WhatsApp Image Keys', 'utf8')
const expanded = Buffer.from(crypto.hkdfSync('sha256', mediaKey, Buffer.alloc(0), info, 112))
return {
iv: expanded.slice(0, 16),
cipherKey: expanded.slice(16, 48),
macKey: expanded.slice(48, 80),
}
}
function decryptMedia(encrypted, mediaKey, mediaType) {
const { iv, cipherKey, macKey } = deriveMediaKeys(mediaKey, mediaType)
const ciphertext = encrypted.slice(0, -10)
const fileMac = encrypted.slice(-10)
// Verify MAC
const hmac = crypto.createHmac('sha256', macKey)
hmac.update(iv)
hmac.update(ciphertext)
const computed = hmac.digest().slice(0, 10)
if (!computed.equals(fileMac)) throw new Error('MAC mismatch — corrupt file or wrong key')
// Decrypt
const decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, iv)
return Buffer.concat([decipher.update(ciphertext), decipher.final()])
}
function downloadBuffer(url) {
return new Promise((resolve, reject) => {
const lib = url.startsWith('https') ? https : http
const req = lib.get(url, { headers: { 'User-Agent': 'WhatsApp/2.26.7.75 A' } }, res => {
if (res.statusCode !== 200) { res.resume(); return reject(new Error('HTTP ' + res.statusCode)) }
const chunks = []
res.on('data', c => chunks.push(c))
res.on('end', () => resolve(Buffer.concat(chunks)))
res.on('error', reject)
})
req.on('error', reject)
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')) })
})
}
async function downloadAndDecrypt(msgId, mediaType, url, mediaKey, opts) {
const extensions = { image: '.jpg', video: '.mp4', audio: '.ogg', voice: '.ogg',
document: '', sticker: '.webp' }
let ext = extensions[mediaType] || ''
if (mediaType === 'document' && opts && opts.fileName) ext = path.extname(opts.fileName) || '.bin'
const encrypted = await downloadBuffer(url)
const decrypted = decryptMedia(encrypted, mediaKey, mediaType)
const outPath = path.join('./media', msgId + ext)
fs.mkdirSync('./media', { recursive: true })
fs.writeFileSync(outPath, decrypted)
return outPath
}Using it in the message event:
const MEDIA_TYPES = new Set(['image', 'video', 'audio', 'voice', 'document', 'sticker'])
client.on('message', async msg => {
const d = msg.decoded
if (!d) return
// Resolve the real phone JID (works even with LID from-fields)
const spn = msg.node && msg.node.attrs && msg.node.attrs.sender_pn
const senderJid = spn ? (spn.user + '@s.whatsapp.net') : msg.from
if (d.type === 'text') {
console.log('text from', senderJid, ':', d.text)
}
if (MEDIA_TYPES.has(d.type) && d.url && d.mediaKey) {
try {
const filePath = await downloadAndDecrypt(msg.id, d.type, d.url, d.mediaKey, { fileName: d.fileName })
console.log('saved', d.type, 'to', filePath)
} catch (e) {
console.error('media download failed:', e.message)
}
}
})Sending Messages
Text Message
await client.sendText('[email protected]', 'Hello!')Quote Message
For the simplest quoted reply use sendReply. If you need low-level control (e.g. quoting a non-text message), pass a contextInfo object directly into sendText:
// low-level: pass contextInfo manually inside sendText options
await client.sendText(
'[email protected]',
'This is a reply',
{
contextInfo: {
quotedMessageId: 'ABCDEF123456', // ID of the quoted message
participant: '[email protected]', // sender of the quoted message
remoteJid: '[email protected]', // chat JID
}
}
)Mention User
await client.sendText(
'[email protected]',
'@919634847671 hello!',
{ mentions: ['[email protected]'] }
)Reaction Message
// react to a message
await client.sendReaction('[email protected]', 'MSGID123', '👍')
// remove a reaction — pass empty string
await client.sendReaction('[email protected]', 'MSGID123', '')Edit Message
[!NOTE] Editing is only possible within 15 minutes of the original send.
await client.editMessage(
'MSGID123', // original message ID
'[email protected]',
'Corrected text here'
)Delete Message
// delete for yourself only
await client.deleteMessage('MSGID123', '[email protected]', true, false)
// delete for everyone (revoke)
await client.deleteMessage('MSGID123', '[email protected]', true, true)Forward Message
Forward text or a full media message (image, video, audio, document, sticker) without
re-uploading. Pass a decoded message object from the message event to forward any media type.
// Forward text
await client.forwardMessage('[email protected]', 'text to forward')
// Forward any received message (full media, no re-upload)
client.on('message', async (msg) => {
if (msg.decoded && msg.decoded.type !== 'text') {
await client.forwardMessage('[email protected]', msg)
}
})Poll
Send a WhatsApp poll. selectableCount is how many options a voter may choose (0 = any).
const { id, encKey } = await client.sendPoll(
'[email protected]',
'Best language?',
['JavaScript', 'Python', 'Rust'],
1 // voters may pick 1 option (0 = unlimited)
)
// encKey (32-byte Buffer) is needed to decrypt incoming poll votesQuoted Reply
Send a text message that quotes (replies to) a specific earlier message. The recipient sees the original message highlighted above your reply.
// DM: senderJid is the same as the chat JID
await client.sendReply(
'[email protected]', // chat JID
'3EB0XXXXXXXX', // ID of the quoted message
'[email protected]', // sender of the quoted message (same as chat for DMs)
'Got it, thanks!' // your reply text
)
// Group: senderJid is the group member who sent the quoted message
await client.sendReply(
'[email protected]', // group JID
'3EB0XXXXXXXX', // ID of the quoted message
'[email protected]', // who sent the original message
'Agreed!'
)You can get the id of a received message from msg.id inside the message event.
Location Message
Send a GPS location pin. name and address are optional labels shown below the map preview.
// minimal — lat/lon only
await client.sendLocation('[email protected]', 48.8566, 2.3522)
// with name and address
await client.sendLocation('[email protected]', 48.8566, 2.3522, {
name: 'Eiffel Tower',
address: 'Champ de Mars, 5 Av. Anatole France, Paris'
})
// to a group
await client.sendLocation('[email protected]', 51.5074, -0.1278, {
name: 'London'
})Contact Message (vCard)
Send a contact card using the standard vCard v3 format. The recipient can save the contact directly from WhatsApp.
const vcard = [
'BEGIN:VCARD',
'VERSION:3.0',
'FN:Alice Smith',
'TEL;TYPE=CELL:+919634847671',
'EMAIL:[email protected]',
'END:VCARD'
].join('\n')
await client.sendContact('[email protected]', 'Alice Smith', vcard)Media Messages
Image Message
// from file path
await client.sendImage('[email protected]', './photo.jpg', { caption: 'Look at this' })
// from Buffer
await client.sendImage('[email protected]', buffer, {
caption: 'Photo',
mimetype: 'image/jpeg'
})Video Message
await client.sendVideo('[email protected]', './clip.mp4', { caption: 'Watch this' })Audio Message
await client.sendAudio('[email protected]', './song.mp3')Voice Note
// ptt: true renders the audio as a push-to-talk voice note with waveform
await client.sendAudio('[email protected]', './voice.ogg', { ptt: true })Document Message
await client.sendDocument('[email protected]', './report.pdf', {
fileName: 'Q1 Report.pdf'
})Sticker Message
await client.sendSticker('[email protected]', './sticker.webp')Status / Stories
// post a text Status to status@broadcast
await client.sendStatus('Good morning!')Send States in Chat
Reading Messages
// mark all messages in a chat as read (sends IQ to server)
await client.markChatRead('[email protected]')Mark Voice Message Played
Send a played receipt for a received voice note (push-to-talk audio). This tells the sender that you have listened to the message.
// msgId: ID of the audio message, from: JID of the sender
client.markMessagePlayed('3EB0ABCDEF123456', '[email protected]')Update Presence
// set yourself as online / offline globally
client.setOnline(true)
client.setOnline(false)
// show typing or recording in a specific chat
client.setChatPresence('[email protected]', 'composing') // typing
client.setChatPresence('[email protected]', 'recording') // recording audio
client.setChatPresence('[email protected]', 'paused') // stoppedModifying Chats
Archive / Unarchive a Chat
client.archiveChat('[email protected]')
client.unarchiveChat('[email protected]')Mute / Unmute a Chat
await client.muteChat('[email protected]', 8 * 60 * 60 * 1000) // mute for 8 hours (ms)
await client.muteChat('[email protected]', 0) // mute indefinitely
await client.unmuteChat('[email protected]')Mark a Chat Read / Unread
await client.markChatRead('[email protected]') // sends IQ to server
client.markChatUnread('[email protected]') // local state onlyPin / Unpin a Chat
client.pinChat('[email protected]')
client.unpinChat('[email protected]')Star / Unstar a Message
client.starMessage('MSGID123', '[email protected]')
client.unstarMessage('MSGID123', '[email protected]')Disappearing Messages
| Duration | Seconds | |---|---| | Off | 0 | | 24 hours | 86 400 | | 7 days | 604 800 | | 90 days | 7 776 000 |
// set disappearing timer for a specific chat (DM or group)
await client.changeEphemeralTimer('[email protected]', 86400)
await client.changeEphemeralTimer('[email protected]', 604800)
// remove disappearing messages
await client.changeEphemeralTimer('[email protected]', 0)User Queries
Check If a Number Has WhatsApp
const { checkNumberStatus } = require('whalibmob')
const result = await checkNumberStatus('919634847671')
// result.status: 'registered' | 'registered_blocked' | 'not_registered' | 'cooldown' | 'unknown'
console.log(result.status)Check multiple numbers at once while connected:
const results = await client.hasWhatsapp(['919634847671', '12345678901'])
// returns array of JIDs that have WhatsAppFetch Profile About
const about = await client.queryAbout('[email protected]')
console.log(about)Fetch Profile Picture
const url = await client.queryPicture('[email protected]')
// also works for groups
const groupUrl = await client.queryPicture('[email protected]')Subscribe to Presence
// triggers 'presence' events when the contact comes online or goes offline
client.subscribeToPresence('[email protected]')
client.on('presence', ({ from, available }) => {
console.log(from, available ? 'online' : 'offline')
})Change Profile
Change Display Name
client.changeName('My Bot')Change About Text
await client.changeAbout('Available 24/7')Change Profile Picture
Both methods accept a Buffer (use fs.readFileSync to load a file).
const fs = require('fs')
// change your own profile picture
await client.changeProfilePicture(fs.readFileSync('./avatar.jpg'))
// change a group's picture (you must be admin)
await client.changeGroupPicture('[email protected]', fs.readFileSync('./group.jpg'))Privacy
Block / Unblock User
await client.blockContact('[email protected]')
await client.unblockContact('[email protected]')Get Block List
const list = await client.queryBlockList()
console.log(list) // [ '[email protected]', ... ]Update Privacy Settings
// type: 'last_seen' | 'profile_picture' | 'status' | 'online' | 'read_receipts' | 'groups_add'
// value: 'all' | 'contacts' | 'contact_blacklist' | 'none' | 'match_last_seen'
await client.changePrivacySetting('last_seen', 'contacts')
await client.changePrivacySetting('profile_picture', 'contacts')
await client.changePrivacySetting('status', 'contacts')
await client.changePrivacySetting('online', 'match_last_seen')
await client.changePrivacySetting('read_receipts', 'none')
await client.changePrivacySetting('groups_add', 'contacts')Update Default Disappearing Mode
// sets the default ephemeral timer for all new chats
await client.changeNewChatsEphemeralTimer(86400) // 1 day
await client.changeNewChatsEphemeralTimer(0) // offGroups
Create a Group
Returns the same metadata object as getGroupMetadata (jid, subject, participants, etc.).
const group = await client.createGroup('My Group', [
'[email protected]',
'[email protected]'
])
console.log('created', group.jid) // '[email protected]'
console.log('subject', group.subject) // 'My Group'
console.log('members', group.participants.map(p => p.jid))Add / Remove or Demote / Promote
const groupJid = '[email protected]'
await client.addGroupParticipants(groupJid, ['[email protected]'])
await client.removeGroupParticipants(groupJid, ['[email protected]'])
await client.promoteGroupParticipants(groupJid, ['[email protected]'])
await client.demoteGroupParticipants(groupJid, ['[email protected]'])Change Subject
await client.changeGroupSubject('[email protected]', 'New Group Name')Change Description
await client.changeGroupDescription('[email protected]', 'This is the group description')Change Settings
// setting: 'edit_group_info' | 'send_messages' | 'add_participants' | 'approve_participants'
// policy: 'admins' | 'all'
await client.changeGroupSetting('[email protected]', 'send_messages', 'admins')
await client.changeGroupSetting('[email protected]', 'edit_group_info', 'admins')
await client.changeGroupSetting('[email protected]', 'add_participants', 'all')Leave a Group
await client.leaveGroup('[email protected]')Get Invite Code
const link = await client.queryGroupInviteLink('[email protected]')
// e.g. 'https://chat.whatsapp.com/AbCdEfGhIjK'
console.log(link)Revoke Invite Code
await client.revokeGroupInvite('[email protected]')Join Using Invitation Code
[!NOTE] Pass only the code portion — do not include
https://chat.whatsapp.com/
const jid = await client.acceptGroupInvite('AbCdEfGhIjK')
console.log('joined', jid)Query Invite Info from Link
Fetch a group's metadata from an invite code or full URL without joining the group. Useful for displaying a preview to the user before they confirm.
// bare code
const info = await client.queryGroupInviteInfo('AbCdEfGhIjKlMnOpQrStUv')
// or pass the full URL — the code is extracted automatically
const info = await client.queryGroupInviteInfo('https://chat.whatsapp.com/AbCdEfGhIjKlMnOpQrStUv')
console.log(info)
// {
// jid: '[email protected]',
// subject: 'My Group',
// creator: '[email protected]',
// creation: 1705315800, // Unix timestamp
// description: 'Group description here',
// participants: [
// { jid: '[email protected]', role: 'admin' },
// { jid: '[email protected]', role: 'member' }
// ]
// }Fetch All Groups
Returns an array of metadata objects for every group you are a member of. Each object has the same shape as getGroupMetadata.
const groups = await client.fetchAllGroups()
for (const g of groups) {
console.log(g.jid, g.subject, g.participants.length + ' members')
}Query Metadata
const meta = await client.getGroupMetadata('[email protected]')
// returns: { jid, subject, creation, creator, subjectTime, subjectBy,
// description, ephemeral, onlyAdminsSend, onlyAdminsEdit, participants[] }
console.log(meta.subject, meta.participants.length + ' members')Get Request Join List
const pending = await client.queryGroupPendingParticipants('[email protected]')
console.log(pending)Approve / Reject Request Join
The second parameter is a boolean: true to approve, false to reject.
// approve join requests
await client.approveGroupParticipants('[email protected]', true, [
'[email protected]'
])
// reject join requests
await client.approveGroupParticipants('[email protected]', false, [
'[email protected]'
])Toggle Ephemeral in Group
await client.changeEphemeralTimer('[email protected]', 86400) // 1 day
await client.changeEphemeralTimer('[email protected]', 0) // offCommunities
WhatsApp Communities are a superset of groups — a parent container that can hold multiple linked sub-groups plus an automatic general-chat group.
Create a Community
const community = await client.createCommunity('My Community', 'A place for discussion')
// community.jid — e.g. [email protected]Deactivate / Delete a Community
await client.deactivateCommunity('[email protected]')Link Groups into a Community
const linked = await client.linkGroupsToCommunity(
'[email protected]', // community JID
['[email protected]', // group JIDs to link
'[email protected]']
)Unlink a Group from a Community
await client.unlinkGroupFromCommunity(
'[email protected]', // community JID
'[email protected]' // group JID
)Newsletters (Channels)
Newsletters are one-to-many broadcast channels. Only the owner can post; anyone can subscribe.
Create a Newsletter
const nl = await client.createNewsletter('Tech News', 'Daily updates on tech')
// nl.jid — e.g. 120363000000000004@newsletterJoin / Leave a Newsletter
await client.joinNewsletter('120363000000000004@newsletter')
await client.leaveNewsletter('120363000000000004@newsletter')Query Newsletter Metadata
const meta = await client.queryNewsletterMetadata('120363000000000004@newsletter')
// { jid, name, description, subscriberCount }Update Newsletter Description
await client.changeNewsletterDescription('120363000000000004@newsletter', 'New description here')Post a Text Update to Your Newsletter
await client.sendNewsletterText('120363000000000004@newsletter', 'Breaking: WhatsApp adds polls!')Business Profile
Query the public business profile of any WhatsApp Business account:
const bp = await client.queryBusinessProfile('[email protected]')
if (bp) {
console.log(bp.category) // e.g. "Software & IT Services"
console.log(bp.email) // business email (if set)
console.log(bp.website) // business website (if set)
console.log(bp.address) // physical address (if set)
console.log(bp.description) // business description (if set)
}
// Returns null if the number is not a WhatsApp Business accountCLI — Getting Started
Install the CLI
Install whalibmob globally to get the wa command available from anywhere on your system:
npm install -g whalibmobVerify the installation:
wa versionFirst-Time Setup: Register a Number
Registration is a one-time process. You need a phone number that can receive an SMS or voice call. Use a dedicated number — do not use a number already active on a real WhatsApp device.
Step 1 — request a verification code
# via SMS (default)
wa registration --request-code 919634847671
# via voice call
wa registration --request-code 919634847671 --method voice
# via an old WhatsApp account
wa registration --request-code 919634847671 --method wa_oldThe CLI sends the code request, prints the result, and then stays open in the interactive shell. You will see:
requesting sms code for +919634847671...
status sent
now run: wa registration --register 919634847671 --code <code>
staying in shell — use /reg confirm 919634847671 <code> to complete
wa>Step 2 — confirm the code you received
Either run the one-shot command:
wa registration --register 919634847671 --code 123456Or type it directly in the shell that stayed open:
wa> /reg confirm 919634847671 123456On success you will see:
registered session saved to /home/user/.waSession/919634847671.json
now run: /connect 919634847671Check if a number already has WhatsApp
wa registration --check 919634847671Output:
checking +919634847671...
status registeredPossible statuses: registered · registered_blocked · not_registered · cooldown · unknown
CLI Connect
After registering, connect with:
wa connect 919634847671The shell opens with a persistent prompt:
connecting to +919634847671...
connected as +919634847671
wa +919634847671>[!TIP] The shell never exits on its own. It stays open until you type
/quitor press Ctrl+C. This is true for every command — registration, connection, sending messages — everything.
Use a custom session directory with --session:
wa connect 919634847671 --session /data/my-sessionsListen Mode
Connect and print all incoming events to the terminal. The process stays alive indefinitely until you press Ctrl+C:
wa listen 919634847671Output as messages arrive:
connected listening on +919634847671 (Ctrl+C to stop)
────────────────────────────────────────────────────────
time 2025-03-13 10:00:05
from [email protected]
id 3EB0ABCDEF123456
text Hello there!CLI — Interactive Shell Commands
After running wa connect <phone>, every feature of the library is available as a /command. Type /help at any time to see all commands.
[!NOTE] JIDs can be written as plain phone numbers (e.g.
919634847671) — the shell automatically appends@s.whatsapp.net. For groups, use the full@g.usJID.
Messaging Commands
Send Text
wa> /send [email protected] Hello, how are you?
sent 3EB0ABCDEF123456
# to a group
wa> /send [email protected] Hello everyone!Send Image
wa> /image [email protected] ./photo.jpg
wa> /image [email protected] ./photo.jpg Look at this!The second argument is the file path. The optional third argument is the caption.
Send Video
wa> /video [email protected] ./clip.mp4
wa> /video [email protected] ./clip.mp4 Watch thisSend Audio
Sends the file as a regular audio attachment:
wa> /audio [email protected] ./song.mp3Send Voice Note
Sends the file as a push-to-talk voice note with waveform:
wa> /ptt [email protected] ./voice.oggSend Document
wa> /doc [email protected] ./report.pdf
wa> /doc [email protected] ./report.pdf "Q1 Report.pdf"The optional third argument overrides the displayed filename.
Send Sticker
The file must be in WebP format:
wa> /sticker [email protected] ./sticker.webpSend Poll (CLI)
Separate the question from the options using |. At least two options are required. Optionally append selectable=N to limit how many options a voter may choose (0 = any):
# single-choice poll (selectable=1)
wa> /poll [email protected] Best language? | JavaScript | Python | Rust | selectable=1
# unlimited-choice poll (default)
wa> /poll [email protected] Pick your favourites | Red | Green | BlueReact to a Message
wa> /react [email protected] 3EB0ABCDEF123456 👍
# remove a reaction — pass a space or empty string
wa> /react [email protected] 3EB0ABCDEF123456 " "The message ID is shown in the incoming message display as id.
Edit a Message
[!NOTE] Editing is only possible within 15 minutes of the original send.
wa> /edit [email protected] 3EB0ABCDEF123456 Corrected text hereDelete a Message
# delete for yourself only
wa> /delete [email protected] 3EB0ABCDEF123456
# delete for everyone (revoke)
wa> /delete [email protected] 3EB0ABCDEF123456 allPost a Status / Story
Posts a text Status visible to your contacts:
wa> /status Good morning everyone!Forward a Message
Sends a message with the forwarded flag set:
wa> /forward [email protected] This message was forwardedReply to a Message (CLI)
Quote and reply to a specific message. You need the message ID (shown as id: in the receive log) and the sender's JID.
# DM — senderJid is the same as the chat JID
wa> /reply [email protected] 3EB0XXXXXXXX [email protected] Got it, thanks!
# Group — senderJid is the member who sent the original message
wa> /reply [email protected] 3EB0XXXXXXXX [email protected] Agreed!The message ID is printed when a message arrives:
id 3EB0C5BA7XXXXXXXXSend Location (CLI)
Send a GPS location pin. Latitude and longitude are required; name and address (separated by |) are optional:
# lat/lon only
wa> /location [email protected] 48.8566 2.3522
# with name
wa> /location [email protected] 48.8566 2.3522 Eiffel Tower
# with name and address (separate with |)
wa> /location [email protected] 48.8566 2.3522 Eiffel Tower | Champ de Mars, Paris
# to a group
wa> /location [email protected] 51.5074 -0.1278 LondonSend Contact / vCard (CLI)
Send a contact card. The vCard string must follow the vCard v3 format. Wrap it in quotes in the shell:
wa> /vcard [email protected] "Alice Smith" "BEGIN:VCARD\nVERSION:3.0\nFN:Alice Smith\nTEL;TYPE=CELL:+919634847671\nEND:VCARD"For multi-line vCards it is easiest to store the string in a shell variable:
VCARD="BEGIN:VCARD
VERSION:3.0
FN:Alice Smith
TEL;TYPE=CELL:+919634847671
EMAIL:[email protected]
END:VCARD"
wa> /vcard [email protected] "Alice Smith" "$VCARD"Presence Commands
Set Online / Offline
wa> /online
wa> /offlineTyping and Recording Indicators
# show "typing…" in a chat
wa> /typing [email protected]
# show "recording audio…" in a chat
wa> /recording [email protected]
# stop the indicator
wa> /stop [email protected]Subscribe to a Contact's Presence
Subscribes to online/offline events for a contact. The shell will print presence updates as they arrive:
wa> /subscribe [email protected]
subscribed to [email protected]
# when they come online:
presence [email protected] onlineProfile Commands
CLI Change Display Name
wa> /name My Bot Name
name updatedCLI Change About Text
wa> /about Available 24/7 for support
about updatedCLI Change Profile Picture
Reads the image from disk and uploads it as your profile picture. Supported formats: JPEG, PNG.
wa> /photo ./avatar.jpg
profile picture updatedCLI Change Privacy Settings
wa> /privacy last_seen contacts
wa> /privacy profile_picture contacts
wa> /privacy status contacts
wa> /privacy online match_last_seen
wa> /privacy read_receipts none
wa> /privacy groups_add contactsAvailable types: last_seen · profile_picture · status · online · read_receipts · groups_add
Available values: all · contacts · contact_blacklist · none · match_last_seen
Contact Commands
Check Who Has WhatsApp
Checks multiple phone numbers (plain digits, no +) and lists which ones are registered on WhatsApp:
wa> /whatsapp 919634847671 12345678901
has whatsapp (1)
[email protected]
not found (1)
12345678901Get Profile Picture URL
Returns the CDN URL for a contact's or group's profile picture:
wa> /picture [email protected]
https://mmg.whatsapp.net/v/...
wa> /picture [email protected]
https://mmg.whatsapp.net/v/...Get Contact About Text
Fetches the bio / about text for a contact:
wa> /contact about [email protected]
Available 24/7Chat Management Commands
Mark Read / Unread
wa> /read [email protected]
wa> /unread [email protected]Mute / Unmute
# mute for 60 minutes
wa> /mute [email protected] 60
# mute indefinitely
wa> /mute [email protected]
# unmute
wa> /unmute [email protected]Pin / Unpin
wa> /pin [email protected]
wa> /unpin [email protected]Archive / Unarchive
wa> /archive [email protected]
wa> /unarchive [email protected]Star / Unstar a Message (CLI)
wa> /star [email protected]