@mr-supun-fernando/supunmd-bail
v3.0.3
Published
WhatsApp API Modification By Supun Fernando
Maintainers
Readme
🧑💻 supunmd-bail 🇱🇰
Open-source WhatsApp automation library — no browser required. Built on WebSocket for speed, stability, and full multi-device support.
Installation • Features • Stability • Website • WhatsApp Channel • YouTube
✨ What is supunmd-bail?
supunmd-bail (@mr-supun-fernando/supunmd-bail) is a powerful, open-source WhatsApp Web library built on top of the Baileys protocol stack — extended with features not found in any other public fork. It connects directly to WhatsApp's multi-device WebSocket protocol. No Selenium, no Puppeteer, no browser overhead.
⚡ Node.js ≥ 20 required.
🚀 Getting Started
npm install @mr-supun-fernando/supunmd-bailimport { makeWASocket, useMultiFileAuthState } from '@mr-supun-fernando/supunmd-bail'
const { state, saveCreds } = await useMultiFileAuthState('auth_info')
const sock = makeWASocket({
auth: state,
syncFullHistory: false, // skip history sync — bot works immediately
aiLabel: true, // stamp messages with AI bot marker (default: true)
})
sock.ev.on('creds.update', saveCreds)🧩 Main Features
| Feature | Description |
|---|---|
| 💬 Rich Response (GenAI Bubble) | Send Meta AI-style messages: markdown, code blocks, tables, LaTeX, maps, inline images, HTML — rendered natively as GenAI bubble in WA client |
| 🚫 4-Factor Ban Checker | Detect ban status via 4 independent signals: live registry, public send-page, identity-key probe, device-count signature |
| 🆔 Username Socket (w:mex) | Full WA username API: check availability, set, delete, pin, find by username, fetch recommendations — via WhatsApp's internal Pando/MEX GraphQL protocol |
| 🏘️ Communities Socket | Full community management: create, link/unlink groups, manage participants, invite codes, ephemeral toggle, approval mode |
| 🔕 noSelfSync | Skip syncing outgoing messages to your own other devices — reduces noise and bandwidth. Includes silent-drop guard (throws 421 instead of pretending the send succeeded) |
| ⚙️ Rust-powered Crypto | md5, hkdf, LT-Hash anti-tampering offloaded to whatsapp-rust-bridge native module for raw speed |
| 🗂️ LID Mapping Store | Persistent LID ↔ phone-number bi-directional cache with LRU eviction and in-flight dedup — prevents duplicate USync lookups |
| 🔐 Pre-Key Manager | Concurrency-safe Signal pre-key operations via per-key-type PQueue — no race conditions on key updates/deletions |
| 🔄 Classify Disconnect | Maps every WA disconnect code to { category, shouldReconnect, backoffMs } — properly handles code 515 (restartRequired) as recoverable, not fatal |
| ⏱️ Rate Limiter | Anti-spam pacing calculator: enforces per-minute/hour/day caps, burst allowance, new-chat delays, identical-message dedup |
| 🖼️ Album Message | Send multiple images/videos as a single WA album/grid, with per-item or top-level caption and gifPlayback support |
| 🤖 AI Label Config | aiLabel: true/false in socket config — controls whether outgoing messages carry the biz_bot attribute that WA uses to render the AI icon |
| 🔎 USync Username Protocol | Username resolution baked into USync queries — resolve WA usernames alongside contacts in a single round-trip |
| 📥 Offline Node Processor | Batch-processes pending stanzas received while offline, preventing message loss on reconnect |
| 🔑 Identity Change Handler | Dedicated handler for Signal identity key changes — prevents session corruption when a contact re-registers |
💬 Rich Response (GenAI Bubble)
Send messages that render as Meta AI-style bubbles inside WhatsApp. Supports multiple primitives in a single message:
// Markdown text
await sock.sendMessage(jid, {
richResponse: {
text: '**Hello** from *supunmd-bail*',
responseId: 'optional-uuid'
}
})
// Code block
await sock.sendMessage(jid, {
richResponse: {
code: 'console.log("hello")',
language: 'javascript'
}
})
// Table
await sock.sendMessage(jid, {
richResponse: {
table: {
rows: [
['Name', 'Age'],
['Alice', '25'],
['Bob', '30']
]
}
}
})
// HTML (raw HTML rendered in client)
await sock.sendMessage(jid, {
richResponse: {
text: 'fallback',
html: '<b>bold</b> <a href="https://example.com">link</a>'
}
})
// LaTeX
await sock.sendMessage(jid, {
richResponse: {
latex: 'E = mc^2'
}
})
// Map / location
await sock.sendMessage(jid, {
richResponse: {
map: {
latitude: -6.2,
longitude: 106.8,
zoom: 15,
title: 'Jakarta',
annotations: []
}
}
})
// Inline image
await sock.sendMessage(jid, {
richResponse: {
imageUrl: 'https://example.com/photo.jpg'
}
})Shortcut methods (all accept quoted and options):
await sock.sendTable(jid, 'Title', ['H1','H2'], [['A','B']], quoted)
await sock.sendList(jid, 'Title', ['item1','item2'], quoted)
await sock.sendCodeBlock(jid, 'print("hello")', quoted, { language: 'python' })
await sock.sendLatex(jid, quoted, { latex: 'x^2 + y^2 = z^2' })
await sock.sendRichMessage(jid, submessages, quoted)🖼️ Album Message
Send multiple images/videos grouped into a single WA album:
await sock.sendMessage(jid, {
album: [
{ image: { url: 'https://example.com/a.jpg' } },
{ image: { url: 'https://example.com/b.jpg' }, caption: 'caption foto kedua' },
{ video: { url: 'https://example.com/c.mp4' }, gifPlayback: false }
],
caption: 'caption ini otomatis ke item pertama'
})🚫 Ban Checker
4-factor ban detection against WhatsApp's own servers — no third-party API:
const result = await sock.checkBanStatus('628123456789')
// or: sock.checkBanStatus('[email protected]')
console.log(result)
// {
// status: 'ACTIVE' | 'PROFILE_HIDDEN' | 'LIKELY_ACTIVE' | 'BANNED' | 'OFF_WHATSAPP' | 'UNKNOWN',
// emoji: '🟢' | '🟡' | '🔴' | '❓',
// confidence: 0..1,
// deviceCount: number | null,
// registryExists: boolean | null,
// pageVisible: boolean | null,
// profileName: string | null
// }Four factors checked:
- Factor A — Live registry (
sock.onWhatsApp) - Factor B — Public send-page title/image probe (
api.whatsapp.com) - Factor C — Identity-key device probe via USync (strongest: banned numbers have their keys destroyed)
- Factor D — Device-count signature (2+ = ACTIVE, 1 generic = likely BANNED, 0 = BANNED/OFF_WA)
🆔 Username Socket (w:mex)
Full WA @username API via WhatsApp's internal Pando/MEX GraphQL protocol, query IDs sourced from Java decompile of WA 2.26.17.2:
// Check availability
const res = await sock.checkUsername('myusername')
// { available: true, username } or { available: false, suggestions: [...] }
// Set username
await sock.setUsername('myusername')
// Delete username
await sock.deleteUsername()
// Get your own username
const mine = await sock.getMyUsername()
// Pin/unpin username
await sock.setUsernamePin(true)
// Find user by username
const user = await sock.findUserByUsername('someuser', pin)
// Fetch usernames of your contacts in batch
const usernames = await sock.fetchContactUsernames('[email protected]', '[email protected]')
// Get username suggestions
const suggestions = await sock.getUsernameRecommendations()🏘️ Communities Socket
// Create community
const community = await sock.communityCreate('Community Name', 'Description')
// Create group inside community
await sock.communityCreateGroup('Group Name', [jid1, jid2], communityJid)
// Link/unlink existing group
await sock.communityLinkGroup(groupJid, communityJid)
await sock.communityUnlinkGroup(groupJid, communityJid)
// Fetch linked groups
const groups = await sock.communityFetchLinkedGroups(communityJid)
// Manage participants
await sock.communityParticipantsUpdate(communityJid, [jid1], 'add') // 'add' | 'remove' | 'promote' | 'demote'
// Invite code
const code = await sock.communityInviteCode(communityJid)
await sock.communityRevokeInvite(communityJid)
await sock.communityAcceptInvite(code)
// Settings
await sock.communityToggleEphemeral(communityJid, 86400) // seconds
await sock.communityMemberAddMode(communityJid, 'admin_add')
await sock.communityJoinApprovalMode(communityJid, 'on')
await sock.communityUpdateSubject(communityJid, 'New Name')
await sock.communityUpdateDescription(communityJid, 'New Desc')
await sock.communityLeave(communityJid)🔕 noSelfSync
Skip syncing outgoing messages to your own linked devices. Useful for high-volume bots where sync traffic is unnecessary:
await sock.sendMessage(jid, { text: 'hello' }, { noSelfSync: true })
// Also supported in all shortcut methods:
await sock.sendTable(jid, title, headers, rows, quoted, { noSelfSync: true })
await sock.sendCodeBlock(jid, code, quoted, { noSelfSync: true })Safe by design: if
noSelfSyncwould accidentally drop all recipients (e.g. messaging yourself), the guard kicks in and delivers normally. If the other party has zero resolved devices, it throwsBoom 421instead of silently pretending the message was sent.
🔄 Stability & Disconnect Handling
supunmd-bail ships classifyDisconnect() — maps every WA status code into an actionable result:
import { classifyDisconnect } from '@mr-supun-fernando/supunmd-bail'
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode
const info = classifyDisconnect(code)
console.log(info)
// {
// category: 'fatal' | 'recoverable' | 'rate-limited' | 'unknown',
// shouldReconnect: boolean,
// backoffMs: number, // ms to wait before reconnecting
// message: string,
// code: number
// }
if (info.shouldReconnect) {
setTimeout(() => reconnect(), info.backoffMs)
}
}
})Code map:
| Code | Category | Reconnect | Backoff | |---|---|---|---| | 401, 440 | fatal | ❌ | — | | 515 | recoverable | ✅ | 0 ms (immediate) | | 405 | fatal | ❌ | — | | 408 | recoverable | ✅ | 30 s | | 503 | recoverable | ✅ | 5 min | | 429 | rate-limited | ✅ | 60 s | | 500 | recoverable | ✅ | 5 s | | 503 | recoverable | ✅ | 10 s | | Graceful close | recoverable | ✅ | 2 s | | Unknown | unknown | ✅ | 15 s |
Important: Code 515 (
restartRequired) is WhatsApp's normal post-pairing signal. supunmd-bail correctly treats it as recoverable with 0 ms backoff — other forks incorrectly mark it as fatal, causing bots to stop after first pair.
⏱️ Rate Limiter
Anti-ban pacing calculator — wire into your send path before each sendMessage:
import { RateLimiter } from '@mr-supun-fernando/supunmd-bail'
const limiter = new RateLimiter({
maxPerMinute: 8,
maxPerHour: 200,
maxPerDay: 1500,
minDelayMs: 1500,
maxDelayMs: 5000,
newChatDelayMs: 3000,
maxIdenticalMessages: 3,
burstAllowance: 3
})
async function safeSend(jid, content) {
const delay = await limiter.getDelay(jid, content)
if (delay === -1) throw new Error('Rate limit exceeded — blocked')
if (delay > 0) await new Promise(r => setTimeout(r, delay))
await sock.sendMessage(jid, content)
limiter.record(jid, content)
}📦 Supported Message Types
Every WhatsApp message type is covered:
- Text, image, video, audio, document, sticker, GIF/video note (PTV)
- Location & live location
- Contact / vCard
- Poll (create, vote, add option, poll result)
- Button, list, template, interactive (native flow)
- Album (multiple media in one message)
- Event & event invite
- Group invite, group status, group mention
- Spoiler (blurred) messages
- Reaction & edited message
- View-once media
- Status/story mention & reply
- Newsletter / channel messages
- Payment & payment invite
- All WhatsApp Business types: product, catalog, order, invoice, business profile
await sock.sendMessage(jid, {
album: [
{ image: { url: 'https://example.com/1.jpg' } },
{ video: { url: 'https://example.com/2.mp4' } }
],
caption: 'Album caption'
})await sock.sendMessage(jid, {
poll: {
name: 'Poll Title',
values: ['Option A', 'Option B'],
selectableCount: 1
}
})await sock.sendMessage(jid, {
text: 'Spoiler content',
spoiler: true
})await sock.sendMessage(jid, {
eventMessage: {
name: 'Meeting',
description: 'Weekly sync',
location: { degreesLatitude: -6.2, degreesLongitude: 106.8 },
startTime: Math.floor(Date.now() / 1000) + 3600
}
})🛠️ Additional Methods
// Parse incoming extended/special messages
sock.ev.on('messages.upsert', ({ messages }) => {
const extended = sock.parseExtendedMessageContent(messages[0].message)
if (extended) console.log(extended.type, extended.data)
})
// Business profile
const profile = await sock.getBusinessProfile(jid)
await sock.updateBusinessProfile({ description: 'Open 9-5', email: '[email protected]', category: 'Retail' })
// Get newsletter/channel JID from URL
await sock.newsletterId('https://whatsapp.com/channel/...')
// Refresh media URL for expired messages
await sock.updateMediaMessage(message)🌐 Community & Support
Developer: Supun Fernando
⭐ If this project helped you, consider giving it a star!
Made with ❤️ by Supun Fernando
📄 License
MIT
