@rinv/rinv-baileys
v1.0.2
Published
Rinv Baileys — an independently maintained Baileys distribution with integrated MessageBuilder, interactive messages, carousel/native-flow helpers, and Rinv-specific compatibility work.
Maintainers
Readme
[!IMPORTANT]
@rinv/rinv-baileysis an unofficial WhatsApp Web API library and is not affiliated with, authorized, maintained, sponsored, or endorsed by WhatsApp or Meta.Use this project responsibly and comply with WhatsApp's Terms of Service and applicable laws.
[!NOTE] Features reconstructed from the WhatsApp Web bundle, and how far each one has been verified, are documented in EXPERIMENTAL.md.
[!NOTE] This project is built on top of the Baileys ecosystem and extends it with additional fixes, compatibility changes, interactive messaging support, and an integrated MessageBuilder.
[!CAUTION] The previous project update channel is no longer used. Release information, changelogs, and project announcements are published through the current WhatsApp Channel linked in this README.
📌 Overview
@rinv/rinv-baileys is a modern ESM-focused Baileys fork for WhatsApp Multi-Device development.
The package combines the socket layer, protocol utilities, LID-aware addressing support, and an integrated MessageBuilder in a single dependency. Buttons, native-flow messages, carousels, and AIRich layouts can be used directly from the package without installing a separate builder dependency.
✨ Highlights
| Feature | Description |
|---|---|
| 🔌 Multi-Device | Connect to WhatsApp using the Baileys Multi-Device protocol. |
| 🔐 Pairing Code | Supports normal and custom 8-character pairing codes. |
| 🖱️ Interactive Buttons | Quick reply, URL, copy, call, list/select, location, and other native-flow buttons. |
| 🧱 Integrated MessageBuilder | Button, ButtonV2, Carousel, AIRich, and Toolkit are included in the same package. |
| 🖼️ Albums | Send multiple images/videos as an album message. |
| 📢 Newsletter | Create, follow, update, react to, and fetch WhatsApp Channel/Newsletter data. |
| 👥 Groups | Group creation, participant management, metadata, description updates, and more. |
| 🪪 LID / PN Addressing | Supports modern LID addressing while exposing the PN/JID alternatives supplied by WhatsApp when available. |
| 📷 Profile Picture | Fetch, update, and remove profile pictures. |
| 🤖 AI Rich | Experimental rich-response builder for text, code, tables, media, suggestions, and other layouts. |
| 📦 ESM | ESM-first package designed for Node.js 22+; Node.js 24 is recommended. |
📚 Table of Contents
- Requirements
- Installation
- Import
- Basic Connection
- Pairing Code
- Receive Messages
- LID / PN / JID Addressing
- Send Messages
- External Ad Reply
- Integrated MessageBuilder
- Album Message
- Newsletter / Channel
- Username & About
- Group Management
- Profile Picture
- Useful Exports
- Update WhatsApp Web Version
- Modern WhatsApp Message APIs
- Troubleshooting
- Credits
- License
⚙️ Requirements
- Node.js 22 or newer
- Node.js 24 recommended for development and release workflows
- npm
- A WhatsApp account for pairing
Check your Node.js version:
node -v📦 Installation
Install directly from npm:
npm install @rinv/rinv-baileysRecommended package setup
Use the package directly under its own name:
{
"type": "module",
"dependencies": {
"@rinv/rinv-baileys": "latest"
}
}This package is ESM-first. Use import syntax instead of require().
📥 Import
import makeWASocket from '@rinv/rinv-baileys'Import additional utilities:
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
Button,
ButtonV2,
Carousel,
AIRich,
Toolkit,
MessageBuilder,
MB
} from '@rinv/rinv-baileys'[!NOTE] MessageBuilder is already integrated. You do not need to install
baileys-mbuilderseparately.
🚀 Basic Connection
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason
} from '@rinv/rinv-baileys'
import pino from 'pino'
async function startSock() {
const { state, saveCreds } = await useMultiFileAuthState('./session')
const sock = makeWASocket({
auth: state,
logger: pino({ level: 'silent' })
})
sock.ev.on('creds.update', saveCreds)
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'open') {
console.log('WhatsApp connected')
}
if (connection === 'close') {
const statusCode = lastDisconnect?.error?.output?.statusCode
const shouldReconnect = statusCode !== DisconnectReason.loggedOut
if (shouldReconnect) {
startSock()
} else {
console.log('Session logged out')
}
}
})
return sock
}
startSock()Retry and pairing options
Beyond the usual Baileys options, these control how the socket handles undecryptable messages, rejected sends, and pairing:
| Option | Default | What it does |
|---|---|---|
| maxMsgRetryCount | 3 | decryption retries requested per incoming message |
| retryRequestDelayMs | 250 | wait before asking the sender to re-encrypt |
| maxRetryQueueSize | 64 | messages allowed to queue for retry at once |
| ackRetryDelayMs | 750 | wait before resending after a retryable nack |
| maxAckRetryCount | 3 | resend attempts after a retryable nack |
| pairingCodeTimeoutMs | 180000 | how long a pairing code stays valid |
maxRetryQueueSize is a safety valve, not a throughput knob. A burst of undecryptable messages would otherwise queue without limit and grow the heap; past the cap the extras are acked without a retry. Raising it does not rescue more messages — retryRequestDelayMs is the setting that does, at the cost of pressing the sender harder.
🔐 Pairing Code
Pairing code can be requested after creating the socket.
const phoneNumber = '6281234567890'
if (!state.creds.registered) {
const code = await sock.requestPairingCode(phoneNumber)
console.log('Pairing code:', code)
}The number is normalized before it is used, so +62 812-3456-7890 and 6281234567890 are the same request. What is rejected is a number that cannot be valid: fewer than 6 or more than 15 digits, or a leading 0 — country codes never start with one, so 081234567890 is the local form, not the international one WhatsApp expects.
await sock.requestPairingCode('081234567890')
// Boom 400: phoneNumber must be in international format:
// country code followed by the national number, digits onlyThe request is confirmed by the server
requestPairingCode waits for WhatsApp's answer and only returns once the server has registered the code. A rejection is thrown rather than swallowed, so a code you receive is a code the server actually knows about:
try {
const code = await sock.requestPairingCode(phoneNumber)
console.log('Pairing code:', code)
} catch (error) {
console.log(error.message) // e.g. rate-overlimit, not-allowed
console.log(error.data) // e.g. 429
}The two rejections you are most likely to meet are rate-overlimit — too many attempts, wait before retrying — and a not-allowed variant, meaning link-by-phone-number is not enabled for that account.
One code at a time
A pairing response can only be decrypted by the keys that produced it, so a second request while one is still pending would destroy the first. That is refused with a 409:
try {
await sock.requestPairingCode(phoneNumber)
} catch (error) {
if (error.output?.statusCode === 409) {
console.log('still pending, seconds left:', error.data.secondsLeft)
}
}Call cancelPairingCode() to abandon a pending attempt and request a new one immediately. It returns whether there was anything to cancel:
sock.cancelPairingCode()
const code = await sock.requestPairingCode(phoneNumber)The guard clears itself once the code expires. WhatsApp rotates a pairing code every 3 minutes; adjust with pairingCodeTimeoutMs if you need a different window.
Custom Pairing Code
A custom pairing code must contain exactly 8 characters.
const code = await sock.requestPairingCode(
'6281234567890',
'RINV01'
)
console.log(code)Checking pairing without touching a running bot
script/testpairing.js runs one pairing request against a throwaway session directory, so credentials of a bot that is already connected are never replaced:
node script/testpairing.js 6281234567890 --check-only--check-only reports whether the server accepted the registration and never prints the code — use it anywhere the output can be read by someone else. Drop the flag to print the code and wait for the link to complete.
📩 Receive Messages
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return
const message = messages[0]
if (!message?.message) return
console.log('From:', message.key.remoteJid)
console.log('Message:', message.message)
})Simple text extraction:
sock.ev.on('messages.upsert', async ({ messages }) => {
const m = messages[0]
if (!m?.message) return
const text =
m.message.conversation ||
m.message.extendedTextMessage?.text ||
m.message.imageMessage?.caption ||
m.message.videoMessage?.caption ||
''
console.log(text)
})🪪 LID / PN / JID Addressing
Recent WhatsApp protocol versions may identify users with LID addresses instead of only phone-number JIDs. Do not assume every incoming user identifier ends with @s.whatsapp.net.
Common forms include:
[email protected]
123456789012345@lid
[email protected]
123456789@newsletterFor incoming messages, inspect the key fields provided by WhatsApp:
const key = message.key
console.log('remoteJid:', key.remoteJid)
console.log('remoteJidAlt:', key.remoteJidAlt)
console.log('participant:', key.participant)
console.log('participantAlt:', key.participantAlt)When WhatsApp supplies an alternate PN/JID, remoteJidAlt or participantAlt can be used by applications that prefer phone-number JIDs. Keep the original LID available as well because some protocol operations may still require the address WhatsApp originally supplied.
Use the built-in JID helpers when normalizing identifiers:
import { jidDecode, jidEncode, jidNormalizedUser } from '@rinv/rinv-baileys'
const normalized = jidNormalizedUser(jid)
const decoded = jidDecode(jid)
console.log(normalized)
console.log(decoded)[!IMPORTANT] LID and PN are two address forms for the same account only when WhatsApp provides or your application already knows the mapping. Do not create a fake PN by replacing the
@lidsuffix.
💬 Send Messages
Text
await sock.sendMessage(jid, {
text: 'Hello from Rinv 💜'
})Image
await sock.sendMessage(jid, {
image: { url: 'https://example.com/image.jpg' },
caption: 'Rinv Image'
})Video
await sock.sendMessage(jid, {
video: { url: 'https://example.com/video.mp4' },
caption: 'Rinv Video'
})Document
await sock.sendMessage(jid, {
document: { url: 'https://example.com/file.pdf' },
fileName: 'document.pdf',
mimetype: 'application/pdf'
})Location
await sock.sendMessage(jid, {
location: {
degreesLatitude: -6.200000,
degreesLongitude: 106.816666,
name: 'Jakarta',
address: 'Jakarta, Indonesia'
}
})Poll
await sock.sendMessage(jid, {
poll: {
name: 'Choose one',
values: ['Option A', 'Option B', 'Option C'],
selectableCount: 1
}
})Poll settings
Every switch WhatsApp shows on its own poll composer is available here. The option names do not match the protobuf field names, so they are listed side by side:
| Option | Protobuf field | Default | What it does |
|---|---|---|---|
| selectableCount | selectableOptionsCount | 1 | how many answers one person may pick |
| hideVoter | hideParticipantName | false | hides who voted for what |
| canAddOption | allowAddOption | false | lets recipients add their own options |
| endDate | endTime | none | a Date after which the poll closes |
These four are gated on the receiving account. WhatsApp checks each one against a server-controlled flag, and when a flag is off the recipient does not merely ignore the setting — the entire poll renders as "You received a message that your version of WhatsApp doesn't support". Option images are relayed separately, so a failed poll can look like only the pictures arrived.
The check is on the field being present, not on its value, which is why this library omits
hideParticipantNameandallowAddOptionentirely when you leave them off rather than sendingfalse.
canAddOptionis the least available of the four: WhatsApp Web has no sending gate for it at all, meaning its own composer never offers it, and the receiving flagpoll_add_option_receiving_enabledstill defaults to off. Treat it as experimental.selectableCountis the one setting that is never gated.To find out what a given account supports, send one poll per setting and see which arrive as real polls.
hideVoter and endDate work on photo polls too. Every poll version carries the same PollCreationMessage, so pollCreationMessageV3 holds those fields exactly as V6 does and the receiver reads them from whichever version arrived — but the option images only attach on V3. This library therefore keeps a photo poll on V3 and reserves V6 for text polls:
| Poll | Version sent |
|---|---|
| any option carrying an image | V3, settings included |
| text options + hideVoter / endDate | V6 |
| text options, one answer | V3 |
| text options, several answers | pollCreationMessage |
canAddOption remains the exception: it fails the whole poll wherever its receiving flag is off, images or not.
await sock.sendMessage(jid, {
poll: {
name: 'Where should we eat?',
values: [
{ name: 'Padang', image: { url: './padang.jpg' } },
'Sunda'
],
selectableCount: 2,
hideVoter: true,
endDate: new Date(Date.now() + 24 * 60 * 60 * 1000)
}
})canAddOption is left out of the example on purpose — add it only once you have confirmed the recipient supports it, since it is the one most likely to turn the whole poll into an unsupported placeholder.
Text options and image options can be mixed in the same poll, exactly as the composer allows. An option carrying an image turns the poll into a photo poll; once canAddOption is set, recipients extend it with Poll Add Option.
endDate takes a Date, not a timestamp — it is converted to epoch milliseconds on the way out.
Photo polls do render in groups and one-to-one chats — the phone clients accept them there.
Two caveats worth knowing. WhatsApp Web's own receiver is stricter than the phones:
isPhotoPollReceiverEnabled = msg =>
isNewsletterMsg({ from: msg.from, to: msg.to }) && isNewsletterPhotoPollsReceiverEnabled()so a photo poll that looks right on a phone can show as unsupported in a browser session. And combining image options with hideVoter or endDate moves the message to pollCreationMessageV6; if the images stop appearing once you add those switches, send the photo poll without them.
📰 External Ad Reply
externalAdReply can be attached through contextInfo when you want a standard WhatsApp link-preview style card.
await sock.sendMessage(jid, {
text: 'Rinv Baileys',
contextInfo: {
externalAdReply: {
title: 'Rinv Baileys',
body: 'Modern WhatsApp Multi-Device library',
mediaType: 1,
thumbnailUrl: 'https://example.com/rinv.jpg',
sourceUrl: 'https://www.npmjs.com/package/@rinv/rinv-baileys',
renderLargerThumbnail: true,
showAdAttribution: false
}
}
})The payload can also be passed to a builder using .setContextInfo(...) when the builder supports context information.
🧱 Integrated MessageBuilder
MessageBuilder v4.6 is included directly inside @rinv/rinv-baileys.
Available exports:
import {
Button,
ButtonV2,
Carousel,
AIRich,
Toolkit,
MessageBuilder,
MB,
MESSAGE_BUILDER_VERSION
} from '@rinv/rinv-baileys'You can also access the classes through MessageBuilder or its short alias MB:
const button = new MessageBuilder.Button(sock)
const carousel = new MB.Carousel(sock)Button
The Button builder is intended for native-flow interactive messages.
Quick Reply + URL + Copy
import { Button } from '@rinv/rinv-baileys'
const message = new Button(sock)
.setTitle('Rinv Menu')
.setBody('Choose an option below.')
.setFooter('@rinv/rinv-baileys')
.addReply('Ping', 'ping')
.addUrl('Open Website', 'https://example.com')
.addCopy('Copy Code', 'RINV2026')
await message.send(jid)Button with Image
const message = new Button(sock)
.setImage('https://example.com/rinv.jpg')
.setTitle('Rinv')
.setBody('Interactive message with image header.')
.setFooter('Powered by Rinv Baileys')
.addReply('Menu', 'menu')
.addUrl('Website', 'https://example.com')
await message.send(jid)Available Button Helpers
.addReply(displayText, id)
.addUrl(displayText, url)
.addCopy(displayText, copyCode)
.addCall(displayText, id)
.addReminder(displayText, id)
.addCancelReminder(displayText, id)
.addAddress(displayText, id)
.addLocation(options)
.addSelection(title, options)
.addButton(name, params)The builder also provides:
.setTitle(text)
.setSubtitle(text)
.setBody(text)
.setFooter(text)
.setImage(urlOrBuffer)
.setVideo(urlOrBuffer)
.setDocument(urlOrBuffer)
.setMedia(object)
.setContextInfo(object)
.addPayload(object)
.clearButtons()
.setParams(object)
.build(jid)
.send(jid)Selection / List
Create a native single-select list using addSelection, makeSection, and makeRow.
const list = new Button(sock)
.setTitle('Rinv Menu')
.setBody('Select one menu.')
.setFooter('Rinv Baileys')
.addSelection('Open Menu')
.makeSection('Main Menu')
.makeRow('', 'Profile', 'Open profile menu', 'profile')
.makeRow('', 'Settings', 'Open settings menu', 'settings')
.makeSection('Other')
.makeRow('', 'About', 'About this bot', 'about')
await list.send(jid)ButtonV2
ButtonV2 provides a simpler classic button builder.
import { ButtonV2 } from '@rinv/rinv-baileys'
const message = new ButtonV2(sock)
.setTitle('Rinv')
.setSubtitle('WhatsApp Bot')
.setBody('Choose an action.')
.setFooter('Rinv Baileys')
.setThumbnail('https://example.com/rinv.jpg')
.addButton('Menu', 'menu')
.addButton('Ping', 'ping')
await message.send(jid)Carousel
Carousel cards can be created from Button.toCard() and then passed to Carousel.
import { Button, Carousel } from '@rinv/rinv-baileys'
const card1 = await new Button(sock)
.setImage('https://example.com/card1.jpg')
.setBody('First card')
.addReply('Select', 'card_1')
.toCard()
const card2 = await new Button(sock)
.setImage('https://example.com/card2.jpg')
.setBody('Second card')
.addUrl('Open', 'https://example.com')
.toCard()
const carousel = new Carousel(sock)
.setBody('Choose one of the cards below.')
.setFooter('Rinv Carousel')
.addCard([card1, card2])
await carousel.send(jid)[!IMPORTANT] Each carousel card must contain an image or video media attachment in its header.
AIRich
AIRich is the integrated rich-response builder for multiple layouts and content types
Text + Code + Table
import { AIRich } from '@rinv/rinv-baileys'
const rich = new AIRich(sock)
.setTitle('Rinv AI')
.setFooter('Generated with AIRich')
.addText('Hello! This is a rich response.')
.addCode('javascript', `console.log('Hello Rinv')`)
.addTable([
['Feature', 'Status'],
['Button', 'Available'],
['Carousel', 'Available'],
['AIRich', 'Experimental']
])
.addSuggest(['Show menu', 'Help me', 'About Rinv'])
await rich.send(jid)Other available AIRich helpers include:
.addText(text)
.addCode(language, code)
.addTable(rows)
.addSource(sources)
.addReels(items)
.addImage(imageUrl, options)
.addVideo(videoUrl, options)
.addProduct(data)
.addPost(data)
.addTip(text)
.addSuggest(suggestion, options)
.addSection(section)
.addSubmessage(submessage)[!WARNING] AIRich and some experimental interactive payloads depend on WhatsApp client/server compatibility. Rendering may change between WhatsApp versions.
🖼️ Album Message
Send multiple images or videos as one album.
await sock.sendMessage(jid, {
album: [
{
image: { url: 'https://example.com/1.jpg' },
caption: 'Image 1'
},
{
image: { url: 'https://example.com/2.jpg' },
caption: 'Image 2'
},
{
video: { url: 'https://example.com/3.mp4' },
caption: 'Video 3'
}
]
})An album requires at least two image/video media items.
📢 Newsletter / Channel
Create Newsletter
const newsletter = await sock.newsletterCreate(
'Rinv Updates',
'Official update channel'
)
console.log(newsletter)Update Name
await sock.newsletterUpdateName(
'123456789@newsletter',
'Rinv News'
)Update Description
await sock.newsletterUpdateDescription(
'123456789@newsletter',
'Fresh updates from Rinv'
)Update Picture
await sock.newsletterUpdatePicture(
'123456789@newsletter',
{ url: 'https://example.com/channel.jpg' }
)Follow / Unfollow
await sock.newsletterFollow('123456789@newsletter')
await sock.newsletterUnfollow('123456789@newsletter')Mute / Unmute
await sock.newsletterMute('123456789@newsletter')
await sock.newsletterUnmute('123456789@newsletter')React to Newsletter Message
await sock.newsletterReactMessage(
'123456789@newsletter',
'175',
'🔥'
)Remove a reaction by using an empty value:
await sock.newsletterReactMessage(
'123456789@newsletter',
'175',
''
)Fetch Newsletter Metadata
const metadata = await sock.newsletterMetadata(
'jid',
'123456789@newsletter'
)
console.log(metadata)Fetch Subscribed Newsletters
const newsletters = await sock.newsletterSubscribed()
console.log(newsletters)Admin Capabilities
Which channel features the server has enabled for you. This is the gate WhatsApp Web itself checks before offering a feature.
const capabilities = await sock.newsletterAdminCapabilities('123456789@newsletter')
console.log(capabilities)
// [ 'INSIGHTS', 'ADMIN_NOTIFICATIONS', 'PHOTO_POLLS', 'QUESTIONS', 'QUIZ', 'THREAD_MENU' ]Requires admin or owner rights on the channel; other channels answer Not Authorized.
Admin Profile Info
const info = await sock.newsletterAdminInfo('123456789@newsletter')Pin / Unpin Messages
Takes the message server_id, not the message key.
await sock.newsletterPinMessages('123456789@newsletter', [175])
await sock.newsletterUnpinMessages('123456789@newsletter', 175)Poll Voters
const voters = await sock.newsletterPollVoters('123456789@newsletter', 175, {
limit: 100,
voteHash: undefined
})The response groups voters per vote_hash, each with a voter_list.edges array.
Reaction Senders
const senders = await sock.newsletterReactionSenders('123456789@newsletter', 175)Content Labels
await sock.newsletterLabelAiContent('123456789@newsletter', 175)
await sock.newsletterLabelPaidPartnership('123456789@newsletter', 175)messageType is the third argument and defaults to MESSAGE; pass STATUS to label a channel status.
Admin Invites
await sock.newsletterCreateAdminInvite('123456789@newsletter', '[email protected]')
await sock.newsletterRevokeAdminInvite('123456789@newsletter', '[email protected]')
await sock.newsletterAcceptAdminInvite('123456789@newsletter')Discovery
const recommended = await sock.newsletterRecommended({ limit: 20, countryCodes: ['ID'] })
const similar = await sock.newsletterSimilar('123456789@newsletter', { limit: 20 })Directory
Channel discovery, the same queries the Updates tab uses. Categories are BUSINESS, ENTERTAINMENT, LIFESTYLE, NEWS, ORGANIZATIONS, PEOPLE, SPORTS and SPECIAL_EVENTS through SPECIAL_EVENTS_5.
const list = await sock.newsletterDirectoryList({
view: 'RECOMMENDED', // RECOMMENDED | NEW | POPULAR | FEATURED | TRENDING
categories: ['NEWS'],
countryCodes: ['ID'],
limit: 20
})
const found = await sock.newsletterDirectorySearch('rinv', { limit: 20 })
const preview = await sock.newsletterDirectoryCategories({ categories: ['NEWS'], countryCode: 'ID' })Vote on a Channel Poll
Channel votes are sent unencrypted as option hashes, unlike the encrypted votes used in chats.
await sock.newsletterSendPollVote('123456789@newsletter', pollServerId, ['Jakarta'])Insights
Admin analytics for a channel you own.
const insights = await sock.newsletterInsights('123456789@newsletter', {
metrics: ['NET_FOLLOWS', 'UNFOLLOWS']
})
// { result: [{ id, values }], last_update_time, metrics_status }metrics_status is OK or MISSING; MISSING means the server has no data for the requested window yet.
Followers
const followers = await sock.newsletterFollowers('123456789@newsletter', { count: 100 })Pending Admin Invites
const pending = await sock.newsletterPendingAdminInvites('123456789@newsletter')Hide a Question Response
Moderates a follower's answer to a channel question.
await sock.newsletterQuestionResponseState('123456789@newsletter', questionServerId, responseServerId, 'HIDDEN')
await sock.newsletterQuestionResponseState('123456789@newsletter', questionServerId, responseServerId, 'VISIBLE')🪪 Username & About
WhatsApp Web moved usernames and the About text to MEX queries. These call the same persisted queries the Web client uses.
Username
const current = await sock.getUsername()
console.log(current) // { username: 'rinv', state: 'ACTIVE', pin: '1234' }
await sock.setUsername('rinv')
await sock.setUsernamePin('1234')
await sock.removeUsername()Check a name before claiming it:
const { available, suggestions } = await sock.checkUsernameAvailability('rinv')setUsername resolves true only when the server answers SUCCESS. state is ACTIVE or RESERVED; pass { reserved: true } when claiming a reserved name.
About / Text Status
await sock.updateTextStatus('Building bots', { emoji: '🤖', ephemeralDurationSec: 0 })
const mine = await sock.fetchTextStatus(['[email protected]'])
const about = await sock.fetchAbout('[email protected]')
console.log(about.status)updateTextStatus() with no text clears it. fetchTextStatus takes one or many JIDs and answers per JID with the text, emoji, last update time and ephemeral duration. fetchAbout reads a single user's About through xwa2_users_updates_since.
The classic updateProfileStatus IQ still works and is untouched.
Terms of Service Notices
WhatsApp gates some features behind a notice the user has to move through. These read the notice list and report progress back, the same IQs the Web client uses.
const notices = await sock.fetchUserNotices()
// [ { id: '20601216', stage: '2', t: '...', version: '...', type: '...' } ]
await sock.updateUserNoticeStage('20601216', 5)stage is the server's own counter for that notice — read the current value from fetchUserNotices before advancing it.
Marketing Opt-Out List
const list = await sock.fetchOptOutList({ category: 'marketing' })
await sock.updateOptOut({
jid: '[email protected]',
category: 'marketing',
action: 'add',
reason: 'user_request'
})Push Settings
const settings = await sock.fetchPushSettings()Server-side Link Preview
Lets WhatsApp generate the preview instead of scraping the page yourself.
const preview = await sock.fetchServerLinkPreview('https://example.com')
// { direct_path, hash, title, description, preview_type, thumb_data, width, height }👥 Group Management
Create Group
const group = await sock.groupCreate(
'Rinv Community',
[
'[email protected]',
'[email protected]'
]
)
console.log(group.id)Add Participant
await sock.groupParticipantsUpdate(
groupJid,
['[email protected]'],
'add'
)Remove Participant
await sock.groupParticipantsUpdate(
groupJid,
['[email protected]'],
'remove'
)Promote / Demote
await sock.groupParticipantsUpdate(groupJid, [userJid], 'promote')
await sock.groupParticipantsUpdate(groupJid, [userJid], 'demote')Update Description
await sock.groupUpdateDescription(
groupJid,
'Welcome to Rinv Community 💜'
)📷 Profile Picture
Fetch Profile Picture URL
const url = await sock.profilePictureUrl(jid, 'image')
console.log(url)Update Profile Picture
await sock.updateProfilePicture(jid, {
url: 'https://example.com/profile.jpg'
})Remove Profile Picture
await sock.removeProfilePicture(jid)🧰 Useful Exports
Some commonly used exports include:
import {
makeWASocket,
useMultiFileAuthState,
DisconnectReason,
jidDecode,
jidEncode,
jidNormalizedUser,
generateWAMessageFromContent,
prepareWAMessageMedia,
Button,
ButtonV2,
Carousel,
AIRich,
Toolkit,
MessageBuilder,
MB,
MESSAGE_BUILDER_VERSION
} from '@rinv/rinv-baileys'Check the builder version:
console.log(MESSAGE_BUILDER_VERSION)
console.log(MessageBuilder.VERSION)🔄 Update WhatsApp Web Version
One command performs the whole check:
npm run wa:updateIt reads the pinned revision, fetches the live one, downloads the bundle into .wa-bundle/<revision>/, parses WhatsApp Web's protobuf specs and compares them against WAProto, diffs the new snapshot against the previous one, round-trips every field through the encoder, and writes .wa-bundle/report.md and report.json.
The report ends in one verdict:
| Verdict | Meaning |
|---|---|
| no-change | live revision matches the pinned one |
| bump-only | revision moved, no wire surface changed |
| bump-and-review | revision moved and a wire surface changed — read the diff |
| needs-work | WhatsApp declares protobuf fields WAProto does not |
| blocked | the round-trip encoder failed; do not bump |
Add --apply to bump the pinned revision, which is refused unless the verdict is a bump and the encoder passed.
npm run wa:update -- --applySupporting commands:
| Command | Purpose |
|---|---|
| npm run wa:diff -- <old> <new> | diff two bundle snapshots on their own |
| npm run check:proto | protobuf gap check only |
| npm run sync:proto | add missing protobuf fields to WAProto |
| npm run verify:proto | round-trip encoder only |
| npm run fetch:bundle -- <dir> | download the raw bundle |
| npm run update:version | bump the pinned revision without any of the checks |
The diff covers every surface a WhatsApp change can reach the wire through — protobuf specs, stanza tags and attributes, xmlns, MEX operations, media paths — so a release that only moves UI code is reported as exactly that. AGENTS.md documents which surfaces matter and which are client-side noise.
Set PROTO_BUNDLE_DIR to read from a local directory and PROTO_OFFLINE=1 to skip the live revision lookup. Where the built-in fetch is refused, the scripts fall back to curl automatically.
For automated releases, only commit the files actually changed by the updater and package.json. Do not commit node_modules.
Recommended .gitignore entries:
node_modules/
npm-debug.log*If your repository intentionally does not track a lockfile for this library package, add package-lock.json as well. Otherwise, keep the lockfile tracked normally.
🧪 Modern WhatsApp Message APIs
Rinv Baileys exposes helpers for newer protobuf message types already present in the bundled WAProto. These APIs are experimental because WhatsApp can gate rendering or server acceptance by account, platform, or rollout.
import {
makeNewsletterStatusAttribution,
LOCATION_BROADCAST_JID,
isJidLocationBroadcast
} from '@rinv/rinv-baileys'Photo Poll
Give an option an image and the poll is sent as a photo poll: the option images go out as associated messages and each option carries the hash the server expects.
These work in groups and one-to-one chats as well as channels, and hideVoter and endDate can be combined with them — the poll stays on pollCreationMessageV3, which is the version the option images attach to. See Poll settings.
await sock.sendMessage(jid, {
poll: {
name: 'Which cover?',
values: [
{ name: 'Jakarta', image: { url: './jakarta.jpg' } },
{ name: 'Bandung', image: { url: './bandung.jpg' } }
],
selectableCount: 1
}
})Each option image is uploaded and then sent as its own pollCreationOptionImageMessage, linked back to the poll by MEDIA_POLL association. A poll with two image options is three messages on the wire.
Plain string options still send a normal text poll, and the two can be mixed. The rest of the poll switches — multiple answers, hidden voters, add-option, end time — are listed under Poll settings.
Question Message
await sock.sendMessage(jid, {
question: {
text: 'What feature should be added next?'
}
})The same payload can be sent to a newsletter JID. Newsletter questions are keyed by a <meta questiontype> node, which the socket adds automatically:
await sock.sendMessage('123456789@newsletter', {
question: {
text: 'Which update do you want next?'
}
})<message to="123456789@newsletter" id="MESSAGE_ID" type="text">
<meta questiontype="question"/>
<plaintext>PROTO_MESSAGE</plaintext>
</message>questiontype is question when posting a question, response when a follower answers it, and reply when the channel replies to an answer.
Question Response
A follower answering a question. Sent with questiontype="response".
await sock.sendMessage(jid, {
questionResponse: {
key: questionMessage.key,
text: 'MessageBuilder'
}
})Question Reply
The channel replying to an answer, quoting it by the question's server id. Sent with questiontype="reply".
await sock.sendMessage('123456789@newsletter', {
questionReply: {
text: 'Good pick, shipping it next',
serverQuestionId: 175,
quotedQuestion: questionMessage.message, // optional
quotedResponse: responseMessage.message // optional
}
})Status Question Answer
await sock.sendMessage(jid, {
statusQuestionAnswer: {
key: statusQuestion.key,
text: 'Rinv Baileys'
}
})Status Quoted Message
await sock.sendMessage(jid, {
statusQuoted: {
originalStatusId: statusMessage.key,
type: 'QUESTION_ANSWER',
text: 'Quoted status answer'
}
})Status Sticker Interaction
await sock.sendMessage(jid, {
statusStickerInteraction: {
key: statusMessage.key,
stickerKey: 'heart',
type: 'REACTION'
}
})Status Notification
Supported notification types are UNKNOWN, STATUS_ADD_YOURS, STATUS_RESHARE, and STATUS_QUESTION_ANSWER_RESHARE.
await sock.sendMessage(jid, {
statusNotification: {
responseMessageKey: responseMessage.key,
originalMessageKey: statusMessage.key,
type: 'STATUS_RESHARE'
}
})Newsletter Admin Invite
await sock.sendMessage(userJid, {
newsletterAdminInvite: {
newsletterJid: '123456789@newsletter',
newsletterName: 'Rinv Updates',
caption: 'Join as an admin',
inviteExpiration: Math.floor(Date.now() / 1000) + 86400
}
})jpegThumbnail and contextInfo can also be supplied.
Newsletter Follower Invite V2
await sock.sendMessage(userJid, {
newsletterFollowerInvite: {
newsletterJid: '123456789@newsletter',
newsletterName: 'Rinv Updates',
caption: 'Follow this channel'
}
})Newsletter Status Attribution
Rinv Baileys exposes StatusAttribution.Type.NEWSLETTER_STATUS with the channel reshare metadata already present in WAProto.
await sock.sendMessage('status@broadcast', {
image: { url: 'https://example.com/status.jpg' },
caption: 'Shared from Rinv Updates',
newsletterStatus: {
newsletterJid: '123456789@newsletter',
messageId: 42,
duration: 24,
hasMultipleReshares: false
}
}, {
statusJidList: audienceJids
})The attribution can also be created manually.
const attribution = makeNewsletterStatusAttribution({
newsletterJid: '123456789@newsletter',
messageId: 42
})
await sock.sendMessage('status@broadcast', {
text: 'Newsletter status',
contextInfo: {
statusAttributions: [attribution]
}
}, {
statusJidList: audienceJids
})Group Status Reaction
await sock.sendMessage(groupJid, {
groupStatusReaction: {
key: groupStatusMessage.key,
text: '❤️'
}
})The reaction is wrapped in groupStatusMessageV2, allowing the existing relay layer to include group-status metadata.
Poll Add Option
The original poll must have been created with canAddOption: true (see Poll settings). One message carries one option — addOption is a single value in the protobuf, not a list, so send several messages to add several options.
await sock.sendMessage(jid, {
pollAddOption: {
pollCreationMessageKey: pollMessage.key,
option: 'New option'
}
})addOption can be supplied directly when you already have the protobuf option object.
Comment Message
content accepts text or protobuf message fields. Raw protobuf content can be supplied as message.
await sock.sendMessage(jid, {
comment: {
targetMessageKey: targetMessage.key,
content: {
text: 'Comment on this message'
}
}
})Event Invite Message
await sock.sendMessage(jid, {
eventInvite: {
eventId: 'rinv-event-001',
eventTitle: 'Rinv Community Event',
startTime: new Date(Date.now() + 3600000),
endTime: new Date(Date.now() + 7200000),
caption: 'See you there'
}
})Scheduled Call
const created = await sock.sendMessage(jid, {
scheduledCall: {
scheduledTimestampMs: new Date(Date.now() + 3600000),
callType: 'VIDEO',
title: 'Rinv Call'
}
})Cancel a scheduled call with its message key.
await sock.sendMessage(jid, {
scheduledCallEdit: {
key: created.key,
editType: 'CANCEL'
}
})Location Broadcast Identifier
WhatsApp Desktop recognizes location@broadcast separately from status@broadcast. Rinv Baileys exposes the identifier and detector without treating it as normal status fanout.
console.log(LOCATION_BROADCAST_JID)
console.log(isJidLocationBroadcast('location@broadcast'))Low-Level Builders
import {
makeQuestionMessage,
makeQuestionResponseMessage,
makeStatusQuestionAnswerMessage,
makeStatusQuotedMessage,
makeStatusStickerInteractionMessage,
makeStatusNotificationMessage,
makeNewsletterAdminInviteMessage,
makeNewsletterFollowerInviteMessage,
makePollAddOptionMessage,
makeCommentMessage,
makeEventInviteMessage,
makeScheduledCallCreationMessage,
makeScheduledCallEditMessage,
makeGroupStatusReactionMessage,
makeNewsletterStatusAttribution,
makeGroupStatusAttribution
} from '@rinv/rinv-baileys'These helpers return protobuf-compatible message content that can be passed to generateWAMessageFromContent or custom relay logic.
[!IMPORTANT] The inspected WhatsApp Desktop build also exposes schema names related to bot history sharing and identity verification. They are intentionally not added until their protobuf field numbers, parent messages, and wire layout are confirmed. Rinv Baileys does not guess protobuf tags.
🐞 Troubleshooting
Pairing code must be exactly 8 characters
When using a custom pairing code:
await sock.requestPairingCode(phone, 'RINV01')The custom value must contain exactly eight characters.
A pairing code appears but the phone never shows a prompt
Check what the request threw before assuming the notification is at fault. requestPairingCode now waits for the server and reports a rejection instead of returning a code that was never registered:
| Message | Meaning |
|---|---|
| rate-overlimit (429) | too many attempts — wait, retrying makes it worse |
| not-allowed / feature errors | link-by-phone-number is not enabled for that account |
| must be in international format (400) | the number is not <country code><national number> |
| accepted without registering | the server replied without a pairing ref |
| never answered | no reply arrived at all |
If none of these fire and the code is registered, type it manually through WhatsApp → Linked Devices → Link with phone number. If it is accepted there, the registration was fine and only the push notification did not arrive, which is decided server-side.
Verify from outside your bot with node script/testpairing.js <number> --check-only.
A pairing request is refused with 409
Another code is still pending. Wait it out or call sock.cancelPairingCode() first — see Pairing Code.
Socket is required
Builder classes require an active Baileys socket:
const button = new Button(sock)Do not create them without passing sock.
Buttons or AIRich render differently
Interactive WhatsApp payloads may depend on:
- WhatsApp application version
- Web protocol changes
- Account/server rollout
- Message type compatibility
Always test experimental message formats before production use.
LID appears instead of a phone-number JID
This is expected on newer WhatsApp addressing flows. Check participantAlt or remoteJidAlt when available instead of blindly converting @lid into @s.whatsapp.net.
Session logged out
If WhatsApp returns DisconnectReason.loggedOut, remove the invalid local session and pair the account again.
🐞 Found a Bug?
If you encounter a bug or compatibility issue, you can contact the maintainer or follow the WhatsApp Channel for project updates.
🙏 Credits
This project exists thanks to the work of many developers and open-source projects.
Project Maintainer
- RexxHayanasi — maintainer, fork development, integration, fixes, features, and project branding.
Baileys / Upstream
- WhiskeySockets/Baileys — upstream Baileys project and core WhatsApp Web implementation.
- adiwajshing — original Baileys author and early ecosystem work.
Fork / Source Contributions
- Lia Wynn / ItsLia — fork lineage and prior Baileys modifications retained where applicable.
- Kyuu / kiuur — project contributor and support.
Integrated MessageBuilder
The integrated MessageBuilder is based on NIXCODE / Advanced WhatsApp Interactive Message Builder.
- Nixel — original creator of the MessageBuilder implementation. WhatsApp · Channel
- Ahmad tumbuh kembang — MessageBuilder contributor.
The original builder attribution and licensing notices must be respected when modifying or redistributing its source. The builder is integrated into this package so users do not need to install baileys-mbuilder separately.
Open Source Contributors
Thanks to every upstream Baileys contributor, library author, tester, issue reporter, and developer whose work helped make this project possible.
Forking and modifying open-source projects is welcome. Please preserve applicable copyright, license, attribution, and contributor notices.
💜 TQTO
Terima kasih kepada semua pihak yang telah memberikan dukungan, inspirasi, dan kontribusi dalam pengembangan proyek ini.
- Allah SWT — atas rahmat, kemudahan, dan perlindungan-Nya.
- Orang Tua — atas doa dan dukungan yang tiada henti.
- RexxHayanasi — pengembang dan maintainer proyek.
- Seluruh contributor dan komunitas open source yang membantu perkembangan Baileys.
📄 License
This project is distributed under the license included with the repository/package.
Rinv-specific modifications are maintained by RexxHayanasi. Portions of the codebase are derived from Baileys and other open-source work and therefore retain applicable upstream copyright, license, and attribution notices.
Do not remove third-party copyright or attribution notices required by their respective licenses.
