npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

[!IMPORTANT] @rinv/rinv-baileys is 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

  • 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-baileys

Recommended 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-mbuilder separately.


🚀 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 only

The 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@newsletter

For 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 @lid suffix.


💬 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 hideParticipantName and allowAddOption entirely when you leave them off rather than sending false.

canAddOption is 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 flag poll_add_option_receiving_enabled still defaults to off. Treat it as experimental. selectableCount is 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:update

It 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 -- --apply

Supporting 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.