whalibmob
v5.1.18
Published
WhatsApp library for interaction with WhatsApp Mobile API no web
Downloads
2,620
Maintainers
Readme
[!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.
[!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 (iPhone), 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.
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 }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] 3EB0ABCDEF123456
wa> /unstar [email protected] 3EB0ABCDEF123456CLI Disappearing Messages
| Duration | Seconds | |---|---| | Off | 0 | | 24 hours | 86400 | | 7 days | 604800 | | 90 days | 7776000 |
# set 1-day timer on a DM
wa> /ephemeral [email protected] 86400
# set 1-week timer on a group
wa> /ephemeral [email protected] 604800
# turn off
wa> /ephemeral [email protected] 0Default Disappearing Timer
Sets the global default ephemeral timer applied to all new chats:
wa> /ephemeral-default 86400
default ephemeral set 86400
# turn off
wa> /ephemeral-default 0
default ephemeral set 0Accepts the same values as /ephemeral: 0, 86400, 604800, 7776000.
Block / Unblock
wa> /block [email protected]
blocked [email protected]
wa> /unblock [email protected]
unblocked [email protected]Show Block List
wa> /blocklist
blocked (2)
[email protected]
[email protected]Group Commands
CLI Create a Group
wa> /group create MyGroup [email protected] [email protected]
creating group...
created [email protected]
subject MyGroup
members [email protected], [email protected]CLI Leave a Group
wa> /group leave [email protected]
left [email protected]Add / Remove Participants
# add participants
wa> /group add [email protected] [email protected]
# remove participants
wa> /group remove [email protected] [email protected]Multiple participants can be listed, separated by spaces.
Promote / Demote Admins
# promote to admin
wa> /group promote [email protected] [email protected]
# demote from admin
wa> /group demote [email protected] [email protected]Change Group Name
wa> /group subject [email protected] New Group Name
subject updatedChange Group Description
wa> /group desc [email protected] This is the group for project updates
description updatedChange Group Picture
Reads the image from disk and sets it as the group's profile picture. You must be an admin.
wa> /group photo [email protected] ./group-logo.jpg
group picture updatedGet Invite Link
wa> /group invite [email protected]
https://chat.whatsapp.com/AbCdEfGhIjKlMnOpQrStUvRevoke Invite Link
Invalidates the current invite link and generates a new one:
wa> /group revoke [email protected]
invite link revokedJoin a Group by Invite Code
Pass only the code part — do not include https://chat.whatsapp.com/:
wa> /group join AbCdEfGhIjKlMnOpQrStUv
joined [email protected]Query Group Invite Info
Preview a group's metadata from an invite link before joining. Accepts the bare code or the full URL:
wa> /group invite-info AbCdEfGhIjKlMnOpQrStUv
────────────────────────────────────────────────────────
jid [email protected]
subject My Group
creator [email protected]
created 2024-01-15 10:30:00
description Group description here
participants (3)
[email protected] [admin]
[email protected]
[email protected]
────────────────────────────────────────────────────────You can also pass the full link:
wa> /group invite-info "https://chat.whatsapp.com/AbCdEfGhIjKlMnOpQrStUv"Query Group Metadata
wa> /group meta [email protected]
jid [email protected]
subject My Group
description Group description here
creator [email protected]
created 2024-01-15 10:30:00
participants 3
onlyAdminsSend false
onlyAdminsEdit true
ephemeral 0List All Groups
Fetches all groups you are a member of and prints a numbered list:
wa> /groups
groups (3)
1 [email protected] My Project Group (5 members)
2 [email protected] Family Chat (12 members)
3 [email protected] Friends (8 members)List Group Participants
Lists all participants of a group with their roles:
wa> /group participants [email protected]
participants (3)
[email protected] [admin]
[email protected]
[email protected]Pending Join Requests
Lists users who have requested to join a group (only visible when approve_participants is enabled):
wa> /group pending [email protected]
pending (2)
[email protected]
[email protected]Approve / Reject Join Requests
# approve one or more pending members
wa> /group approve [email protected] [email protected]
approved [email protected]
# reject one or more pending members
wa> /group reject [email protected] [email protected]
rejected [email protected]Multiple JIDs can be listed, separated by spaces.
Group Settings
Controls who can send messages, edit group info, add participants, or requires approval to join:
# only admins can send messages
wa> /group settings [email protected] send_messages admins
# everyone can send messages
wa> /group settings [email protected] send_messages all
# only admins can edit group info
wa> /group settings [email protected] edit_group_info admins
# only admins can add participants
wa> /group settings [email protected] add_participants admins
# require admin approval for join requests
wa> /group settings [email protected] approve_participants adminsCommunity Commands (CLI)
Communities group multiple linked groups under one umbrella. Only the community creator can link / unlink groups or deactivate the community.
# create a community (description is optional)
wa> /community create "Dev Squad" "Our developer community"
# link an existing group into the community
wa> /community link [email protected] [email protected]
# unlink a group from the community
wa> /community unlink [email protected] [email protected]
# permanently deactivate (delete) a community
wa> /community deactivate [email protected]Newsletter / Channel Commands
Newsletters are one-to-many broadcast channels. Only the owner can post; anyone can subscribe.
# create a new channel
wa> /newsletter create Tech News Daily tips about technology
# subscribe to a channel
wa> /newsletter join 120363000000000004@newsletter
# unsubscribe from a channel
wa> /newsletter leave 120363000000000004@newsletter
# query channel metadata (name, description, subscriber count)
wa> /newsletter info 120363000000000004@newsletter
────────────────────────────────────────────────────────
jid 120363000000000004@newsletter
name Tech News
description Daily tips about technology
subscribers 1234
────────────────────────────────────────────────────────
# update the channel description (you must be the owner)
wa> /newsletter desc 120363000000000004@newsletter New description here
# post a text update to your channel (you must be the owner)
wa> /newsletter post 120363000000000004@newsletter Breaking: WhatsApp adds polls!Business Profile Command (CLI)
Query the public business profile of any WhatsApp Business account:
wa> /biz [email protected]
────────────────────────────────────────────────────────
jid [email protected]
category Software & IT Services
email [email protected]
website https://example.com
address 123 Main St
description We build software
────────────────────────────────────────────────────────Returns a message if the number is not a WhatsApp Business account.
Registration Commands (in-shell)
These commands work from within the shell — useful when you need to register a second number without closing the current session:
# check if a phone number has WhatsApp
wa> /reg check 919634847671
# request a verification code
wa> /reg code 919634847671
wa> /reg code 919634847671 voice
wa> /reg code 919634847671 wa_old
# confirm the code received
wa> /reg confirm 919634847671 123456
registered session saved to /home/user/.waSession/919634847671.json
now run: /connect 919634847671Connection Commands (in-shell)
# connect to a number (while already in the shell)
wa> /connect 919634847671
# disconnect
wa> /disconnect
# force a reconnection
wa> /reconnect
# show current session info
wa> /session
phone 919634847671
name My Bot
session /home/user/.waSession
# show all available commands
wa> /help
# disconnect and exit the shell
wa> /quitFull Command Reference Table
| Command | Description |
|---|---|
| Messaging | |
| /send <jid> <text> | Send a text message |
| /image <jid> <file> [caption] | Send an image |
| /video <jid> <file> [caption] | Send a video |
| /audio <jid> <file> | Send an audio file |
| /ptt <jid> <file> | Send a voice note (push-to-talk) |
| /doc <jid> <file> [name] | Send a document |
| /sticker <jid> <file> | Send a sticker (.webp) |
| /poll <jid> <question> \| <opt1> \| <opt2> [selectable=N] | Send a poll |
| /react <jid> <msgId> <emoji> | React to a message |
| /edit <jid> <msgId> <text> | Edit a sent message |
| /delete <jid> <msgId> [all] | Delete a message (add all for everyone) |
| /status <text> | Post a Status / Story |
| /forward <jid> <text> | Send with forwarded flag |
| /reply <jid> <msgId> <senderJid> <text> | Reply quoting a specific message |
| /location <jid> <lat> <lon> [name] [| address] | Send a GPS location pin |
| /vcard <jid> <displayName> <vcard> | Send a contact card (vCard v3) |
| Presence | |
| /online | Set yourself as online |
| /offline | Set yourself as offline |
| /typing <jid> | Show typing indicator in a chat |
| /recording <jid> | Show recording audio indicator |
| /stop <jid> | Stop typing / recording |
| /subscribe <jid> | Subscribe to a contact's presence |
| Profile | |
| /name <text> | Change your display name |
| /about <text> | Change your bio / about text |
| /photo <file> | Change your profile picture |
| /privacy <type> <value> | Change a privacy setting |
| Contacts | |
| /whatsapp <phone...> | Check which numbers have WhatsApp |
| /picture <jid> | Get profile picture CDN URL |
| /contact about <jid> | Get bio / about text of a contact |
| Chat Management | |
| /read <jid> | Mark chat as read |
| /unread <jid> | Mark chat as unread |
| /mute <jid> [minutes] | Mute a chat (indefinitely if no minutes given) |
| /unmute <jid> | Unmute a chat |
| /pin <jid> | Pin a chat |
| /unpin <jid> | Unpin a chat |
| /archive <jid> | Archive a chat |
| /unarchive <jid> | Unarchive a chat |
| /star <jid> <msgId> | Star a message |
| /unstar <jid> <msgId> | Unstar a message |
| /ephemeral <jid> <seconds> | Set disappearing messages timer for a chat |
| /ephemeral-default <seconds> | Set global default ephemeral timer for new chats |
| /block <jid> | Block a contact |
| /unblock <jid> | Unblock a contact |
| /blocklist | Show all blocked contacts |
| Groups | |
| /group create <name> <jid...> | Create a group |
| /group leave <jid> | Leave a group |
| /group add <jid> <member...> | Add participants |
| /group remove <jid> <member...> | Remove participants |
| /group promote <jid> <member...> | Promote to admin |
| /group demote <jid> <member...> | Demote from admin |
| /group subject <jid> <name> | Rename group |
| /group desc <jid> <text> | Change group description |
| /group photo <jid> <file> | Change group picture |
| /group invite <jid> | Get invite link |
| /group revoke <jid> | Revoke invite link |
| /group join <code> | Join group by invite code |
| /group invite-info <code\|url> | Preview group metadata from an invite link (without joining) |
| /groups | List all groups you are a member of |
| /group meta <jid> | Query group metadata |
| /group participants <jid> | List group participants with roles |
| /group pending <jid> | List pending join requests |
| /group approve <jid> <member...> | Approve pending join requests |
| /group reject <jid> <member...> | Reject pending join requests |
| /group settings <jid> <setting> <policy> | Change group setting |
| Community | |
| /community create <subject> [description] | Create a community |
| /community deactivate <communityJid> | Permanently delete a community |
| /community link <communityJid> <groupJid> | Link a group into a community |
| /community unlink <communityJid> <groupJid> | Unlink a group from a community |
| Newsletter / Channel | |
| /newsletter create <name> [description] | Create a newsletter channel |
| /newsletter join <jid> | Subscribe to a channel |
| /newsletter leave <jid> | Unsubscribe from a channel |
| /newsletter info <jid> | Query channel metadata |
| /newsletter desc <jid> <text> | Update channel description |
| /newsletter post <jid> <text> | Post a text update to your channel |
| Business | |
| /biz <phone\|jid> | Query business profile of a WhatsApp Business account |
| Registration | |
| /reg check <phone> | Check if number has WhatsApp |
| /reg code <phone> [method] | Request verification code |
| /reg confirm <phone> <code> | Complete registration |
| Connection | |
| /connect <phone> | Connect to WhatsApp |
| /disconnect | Disconnect current session |
| /reconnect | Force reconnection |
| /session | Show session info |
| /help | Show all commands |
| /quit / /exit | Disconnect and exit |
WhatsApp IDs
- Individual contacts:
[countrycode][number]@s.whatsapp.net- Example:
[email protected]
- Example:
- Groups:
[groupid]@g.us- Example:
[email protected]
- Example:
- Status broadcast:
status@broadcast - Broadcast lists:
[timestamp]@broadcast
[!NOTE] Phone numbers must include the country code without the
+prefix.
Transport
whalibmob uses Noise_XX_25519_AESGCM_SHA256 over TCP to g.whatsapp.net:443:
- Client sends
ClientHellowith an ephemeral X25519 public key. - Server replies
ServerHello(ephemeral + encrypted static + payload). - Three DH steps (EE, SE, SS) derive the final session keys.
- Client sends
ClientFinish(encrypted static + encrypted payload). - The library waits for a
<success>stanza before emittingconnected, or<failure>forauth_failure.
Automatic reconnection uses exponential backoff:
| Attempt | Delay | |---|---| | 1 | 1 s | | 2 | 2 s | | 3 | 4 s | | 4 | 8 s | | 5 | 15 s | | 6+ | 30 s |
Media Encryption
Sending (upload flow)
- A random 32-byte media key is generated.
- HKDF-SHA256 expands it into IV (16 bytes), cipher key (32 bytes), and MAC key (32 bytes).
- The file is encrypted with AES-256-CBC.
- A 10-byte HMAC-SHA256 MAC is appended to the ciphertext.
- The encrypted blob is uploaded to the WhatsApp CDN.
- The media key and CDN URL are embedded in the Signal-encrypted message envelope sent to the recipient.
Receiving (download + decrypt flow)
When you receive a media message, msg.decoded contains:
url— the HTTPS CDN link to download the encrypted blobmediaKey— the 32-byte key (as aBuffer) needed to decrypt itdirectPath— CDN path (fallback if full URL is unavailable)
The decryption process mirrors the upload:
| Step | Operation |
|---|---|
| 1 | Download encrypted blob from CDN URL |
| 2 | HKDF-SHA256(mediaKey, "", "WhatsApp <Type> Keys", 112) → expanded key material |
| 3 | Split: [0:16] = IV · [16:48] = AES cipher key · [48:80] = HMAC key |
| 4 | Verify: HMAC-SHA256(macKey, IV ∥ ciphertext)[:10] must equal last 10 bytes of blob |
| 5 | Decrypt: AES-256-CBC(cipherKey, IV, ciphertext) — strip last 10 bytes first |
HKDF info strings:
| Media type | Info string |
|---|---|
| image / sticker | WhatsApp Image Keys |
| video | WhatsApp Video Keys |
| audio / voice | WhatsApp Audio Keys |
| document | WhatsApp Document Keys |
See the Receiving Media section for a complete working code example.
Device Emulation
whalibmob emulates an iPhone when communicating with WhatsApp servers.
The device profile controls the User-Agent header, the Noise Protocol platform field, and the token computation.
When no WA_DEVICE is set, the library picks a random iPhone model from the built-in profile list for each new session. This provides natural device diversity.
Configuration is done entirely through environment variables — no code changes required.
Copy .env.example to .env in your project root and set the variables you need.
Device Quick Start
Emulate an iPhone 16 Pro:
WA_DEVICE=iphone16pro node your-app.jsOr put the variables in a .env file. When using the CLI (wa command) the file is loaded automatically. When using the library directly, load it before require('whalibmob'):
require('dotenv').config() // must be first
const { WhalibmobClient } = require('whalibmob')WA_DEVICE=iphone16proiPhone Profiles
Available values for WA_DEVICE:
| Profile key | Device | iOS version |
|---|---|---|
| iphone16promax | iPhone 16 Pro Max | 18.3.2 |
| iphone16pro | iPhone 16 Pro | 18.3.2 |
| iphone16plus | iPhone 16 Plus | 18.3.2 |
| iphone16 | iPhone 16 | 18.3.2 |
| iphone15promax | iPhone 15 Pro Max | 18.3.2 |
| iphone15pro | iPhone 15 Pro | 18.3.2 |
| iphone15plus | iPhone 15 Plus | 18.3.2 |
| iphone15 | iPhone 15 | 18.3.2 |
| iphone14promax | iPhone 14 Pro Max | 18.3.2 |
| iphone14pro | iPhone 14 Pro | 18.3.2 |
| iphone14plus | iPhone 14 Plus | 17.7.5 |
| iphone14 | iPhone 14 | 17.7.5 |
| iphone13pro | iPhone 13 Pro | 17.7.5 |
| iphone13 | iPhone 13 | 17.7.5 |
| iphone12pro | iPhone 12 Pro | 17.7.5 |
| iphone12 | iPhone 12 | 17.7.5 |
| iphonese3 | iPhone SE (3rd gen) | 17.7.5 |
| iphone11pro | iPhone 11 Pro | 17.7.5 |
| iphone11 | iPhone 11 | 17.7.5 |
| iphonexs | iPhone Xs | 16.7.11 |
User-Agent format: WhatsApp/<version> iOS/<osVersion> Device/<model>
The WhatsApp version is fetched automatically from the Apple App Store (iTunes API) every time you register, always using the latest published version. The result is cached for 6 hours so repeated calls within a session are fast. If all App Store sources fail (no internet, rate limit, etc.), the built-in IOS_VERSION_FALLBACK constant is used as a last resort. Three iTu
