@violetix/baileys
A WhatsApp Web API for Node β Baileys v7 with interactive messages, albums, payment
messages, newsletter media, and the protocol surface upstream does not expose yet.
npm i @violetix/baileys
π Table of contents
π₯ Installation
Requires Node.js 20 or newer. The package uses ES modules (ESM).
Save examples as .mjs files, or set "type": "module" in your application's package.json.
npm i @violetix/baileys
npm i github:cv3inx/bv2
Equivalent package.json entries β one or the other, not both:
{
"dependencies": {
"@violetix/baileys": "^0.3.18"
}
}
{
"dependencies": {
"@violetix/baileys": "github:cv3inx/bv2"
}
}
π§© Import (ESM & CJS)
ES modules:
import makeWASocket, { useMultiFileAuthState } from "@violetix/baileys";
From CommonJS, use dynamic import() inside an async function:
async function main() {
const { makeWASocket, useMultiFileAuthState } = await import("@violetix/baileys");
}
main().catch(console.error);
π Connect to WhatsApp (Quick Step)
-
Install the package and the logger used by this example:
npm install @violetix/baileys pino
-
Save the following code as index.mjs. Replace myPhoneNumber with your WhatsApp number, including the country code and using digits only.
import {
makeWASocket,
delay,
DisconnectReason,
fetchLatestWaWebVersion,
useMultiFileAuthState,
} from "@violetix/baileys";
import pino from "pino";
const myPhoneNumber = "6288888888888";
const logger = pino({ level: "silent" });
async function connectToWhatsApp() {
const { state, saveCreds } = await useMultiFileAuthState("session");
const sock = makeWASocket({
logger,
auth: state,
version: async () => (await fetchLatestWaWebVersion()).version,
});
sock.ev.on("creds.update", saveCreds);
sock.ev.on("connection.update", async ({ connection, lastDisconnect }) => {
try {
if (connection === "close") {
const statusCode = lastDisconnect?.error?.output?.statusCode;
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
console.log("Connection closed:", lastDisconnect?.error);
if (shouldReconnect) {
await delay(3000);
await connectToWhatsApp();
} else {
console.log("Logged out. Pair a new session to connect again.");
}
} else if (connection === "open") {
console.log("Connected to WhatsApp");
}
} catch (error) {
console.error("Connection error:", error);
}
});
sock.ev.on("messages.upsert", async ({ type, messages }) => {
if (type !== "notify") return;
for (const message of messages) {
const jid = message.key.remoteJid;
if (!message.message || message.key.fromMe || !jid) continue;
if (!jid.endsWith("@s.whatsapp.net") && !jid.endsWith("@lid")) continue;
try {
await sock.sendMessage(jid, { text: "Hello world" });
} catch (error) {
console.error("Failed to reply:", error);
}
}
});
if (!state.creds.registered) {
const code = await sock.requestPairingCode(myPhoneNumber);
console.log("Pairing code:", code);
}
}
connectToWhatsApp().catch(console.error);
- Run
node index.mjs, then enter the printed pairing code in WhatsApp's linked-device flow on your phone.
This example replies to new incoming direct messages. Credentials are saved in
session/ and reused on later runs. Keep that directory private and out of version control.
π Auth State
[!NOTE]
You can use the experimental useSingleFileAuthState and useSqliteAuthState as an alternative to useMultiFileAuthState. However, useSingleFileAuthState already includes an internal caching mechanism, so there is no need to wrap state.keys with makeCacheableSignalKeyStore.
ποΈ Implementing Data Store
makeInMemoryStore keeps chats, contacts, and messages in memory. For larger deployments,
use a persistent store appropriate to your application's memory and retention requirements.
Add this import and create the store outside connectToWhatsApp() in the quick-start example,
after the logger declaration, so reconnects reuse the same store:
import { makeInMemoryStore } from "@violetix/baileys";
const storePath = "./store.json";
const store = makeInMemoryStore({ logger });
store.readFromFile(storePath);
setInterval(() => {
try {
store.writeToFile(storePath);
} catch (error) {
console.error("Failed to save the store:", error);
}
}, 180_000).unref();
Add getMessage to the makeWASocket options so retries and poll updates can retrieve
previously stored messages:
getMessage: async (key) => {
if (!key.remoteJid || !key.id) return undefined;
const message = await store.loadMessage(key.remoteJid, key.id);
return message?.message;
},
Immediately after creating each socket, bind its events to the store:
store.bind(sock.ev);
sock.ev.on("chats.upsert", () => {
console.log("Chats:", store.chats.all());
});
sock.ev.on("contacts.upsert", () => {
console.log("Contacts:", Object.values(store.contacts));
});
The file snapshot is saved every three minutes. Keep store.json private as it contains chat data.
πͺͺ WhatsApp IDs Explain
A JID identifies a WhatsApp account, group, or destination. A LID is an account identifier;
it is not a phone number and must not be converted into one by changing its suffix.
| User, addressed by phone number | 19999999999@s.whatsapp.net |
| User, addressed by LID | 12699999999@lid |
| Group | 123456789-123345@g.us |
| Newsletter / channel | 120111111111111111@newsletter |
| Meta AI | 11111111111@bot |
| Broadcast list | [timestamp of creation]@broadcast |
| Status / stories | status@broadcast |
Use the JID returned by message events or lookup APIs instead of constructing one from an assumed phone number.
[!IMPORTANT]
Senders who set a WhatsApp username arrive LID-addressed, and WhatsApp sends no phone number alongside them. This fork recovers the phone number from the local LIDβPN mapping and fills in key.remoteJidAlt (or key.participantAlt in groups).
That recovery only works once a mapping exists β from history sync, group metadata, or an earlier chat. On a first-ever contact from a username user there is genuinely no phone number to recover, so handle @lid natively rather than assuming a phone number is always available.
β‘ Why this fork exists
This is the Baileys build that runs the Violetics bot fleet: roughly 125 WhatsApp sessions inside
one Node process. Everything in the table below came out of that, and most of it is invisible until
you run more than a handful of sockets at once β a bug that costs one session a reconnect costs a
fleet an hour of flapping.
| Disconnect reasons are named | CB:failure closed with the constant message Connection Failure for a 401 device-removal and for a transient 503 alike. A host that re-emits the close as its own error event forwards message and nothing else, so it could not tell a dead session from a blip and reconnected both β one logout became a re-auth loop and the number got banned. end() now appends the code and its DisconnectReason: Connection Failure [401 loggedOut]. |
| Retry caches are bounded | sessionRecreateHistory and retryCounters had a TTL but no max, and one MessageRetryManager exists per socket. retryCounters sets updateAgeOnGet, so a message stuck in a retry loop refreshed its own expiry indefinitely β the opposite of what a TTL backstop is for. |
| Keepalive is per socket, and staggered | A single process-wide 30s timer pinged every socket in the same tick, while the staleness check used the configured interval β so any keepAliveIntervalMs other than 30000 produced false connectionLost closes. Each socket now owns its timer and starts on a random offset. |
| Housekeeping is not gated on presence | The 24-hour tcTokenKnownJids prune only ran while isOnline was true, and isOnline is only ever set by sendPresenceUpdate. A bot that never marks itself online never pruned anything. |
| One failing socket cannot take the host down | A socket that failed to open, and an unhandled rejection out of end() mid-teardown, killed the whole process β and with it every other session sharing it. |
| Signal writes persist before success | Key-store writes are awaited, transaction contexts are isolated per auth store, and session deletion is reflected immediately. Close still waits for the key-store flush barrier. |
| The offline queue drains fully | Baileys asked for one offline_batch of 100 stanzas and never asked for another, so a larger backlog stayed on the server and grew on every reconnect. |
Upstream feature work β newsletter media, interactive messages, albums, the MEX and HTTPS GraphQL
surface β is listed under Upstream Fixes & Improvements
and the sections after it.
π οΈ Upstream Fixes & Improvements
- πΌοΈ Fixed an issue where media could not be sent to newsletters due to an upstream issue.
- π Reintroduced
makeInMemoryStore with a minimal ESM adaptation and small adjustments for Baileys v7.
- π¦ Switched FFmpeg execution from
exec to spawn for safer process handling.
- ποΈ Added
@napi-rs/image as a supported image processing backend in getImageProcessingLibrary(), offering a balance between performance and compatibility.
- πͺͺ Recovered the phone number for LID-addressed senders that arrive without one, so username users no longer show up as a bare
@lid.
- π Routed top-level call stanzas (
<offer>, <terminate>, β¦) that arrive outside a <call> wrapper, and acknowledged them β previously they were never acked, so the server kept redelivering them.
- π€ Decrypted
msmsg Meta AI bot replies instead of dropping them. The messageSecret is captured at send time and recovered from getMessage after a restart.
- π Parsed the USync fields that were previously left as TODOs: per-protocol errors with backoff, cache
refresh hints, side_list, per-contact privacy tokens, and blocked-by-contact detection.
- π Fixed the USync username parser, which returned
null for the Uint8Array payloads the binary decoder actually produces.
- π₯ Drained the whole offline queue on reconnect. Baileys requested a single
offline_batch of 100 stanzas and never asked for another, so any larger backlog stayed on the server and grew on every reconnect.
- π£ Fanned out sends to broadcast lists (
<id>@broadcast) over a sender key, using the same statusJidList option as status posts for the recipient list.
- π Surfaced
keyRequired from onWhatsAppUsername() when the server withholds the account until the 4-digit username key is supplied; previously such rows were dropped.
- π Emitted
username.update (a contact set or removed their @username), lid-mapping.update (a contact's LID rotated; the known phone number is carried over), and privacy.update (privacy settings changed on the phone) from server notifications.
- π Skipped placeholder resend requests for
<unavailable/> messages WhatsApp marks as unrecoverable (<bot/>, hosted="true", type="view_once"); the previous check compared against attribute values the server never sends.
- πͺͺ Recognised own LID / hosted devices as
fromMe in status posts and group notifications, and read remoteJidUsername from the username attribute on 1:1 messages.
- π Requested
fetch_pinned_messages on newsletterMetadata(), so pinned posts come back with the metadata.
- ποΈ Attached the
streamingSidecar WhatsApp Web sends with video and audio uploads, so recipients can seek and play while downloading. Skipped for mp4 files that are not faststart (mdat before moov), where a sidecar would make playback fail instead of falling back to a plain download.
- π Stamped
disappearingMode (and ephemeralSettingTimestamp when passed via sendMessage options) on ephemeral messages, matching WhatsApp Web; without it the peer warns the message will not disappear.
- π³οΈ Added
decryptPollVoteWithFallback() / decryptEventResponseWithFallback(), which try the poll or event creator's PN and LID forms; voters encrypt with whichever form of the creator JID they hold. Event responses now use it internally.
- π Username lookups (
onWhatsAppUsername()) request LID addressing, as WhatsApp Web does; USyncQuery.withContactProtocol('lid' | 'pn') exposes the switch.
- π³οΈ Poll votes are decrypted again inside
processMessage and surfaced as pollUpdates on messages.update (like Baileys v6.4 did), trying the creator's PN and LID forms. Requires getMessage to return the poll creation message.
- π§Ύ
hideVoter / canAddOption polls go out as pollCreationMessageV6, the only version that carries those flags.
- π§ History sync chunks are decoded one conversation at a time instead of materialising the whole
HistorySync tree, cutting peak memory on large syncs. Output is byte-identical to the previous path.
- π’
version accepts a resolver function, and a 405 client_too_old failure fetches the live WA Web build and applies it on the next reconnect. fetchLatestWaWebVersion() now times out after 10s (timeoutMs / signal options).
- π Passkey-gated (Shortcake) accounts are surfaced:
connection.update { passkeyRequired: true } plus a warning when the server sends passkey_prologue_request. Expired pairing codes (refresh_code) are re-registered automatically with the same code, or surfaced as connection.update { pairingCodeExpired: true }.
- π’
onWhatsAppUsername() also reports isBusiness / pnJid; the USync business parser treats a <error/> child as "not a business" instead of failing the whole query, and withBusinessProtocol(null) asks for the verified name only.
- πΌοΈ Added image polls:
sendMessage(jid, { imagePoll }) uploads one image per option, hashes each option over its uploaded fileSha256, sends the pollCreationMessageV3 parent with pollContentType: IMAGE, then one pollCreationOptionImageMessage child per option associated back by MEDIA_POLL. hashImagePollOption() is exported separately. Ported from @rennzsync/baileys.
- π§© Added A2UI widgets:
sendMessage(jid, { a2ui }) with a declarative builder (Text, Image, Video, Button, Card, Column, Row, Divider, CheckBox, TextField, ChoicePicker, plus listCard) compiled into interactiveMessage.bloksWidget. Ported from @rennzsync/baileys, rebuilt on this fork's own interactive path so the <biz> node and button shapes are the existing ones.
- π Added the
exclusive send option: a group message addressed to a single member (to=<group> participant=<member>, per-device encryption instead of the group sender key), so no other member receives the stanza. The recipient comes from the quoted message's author, or from an explicit jid.
- π« Added
sock.ignoreKey(): drops matching inbound stanzas before any handler β including the decryption and persistence paths β while still acking so the server stops redelivering. Matches by remoteJid / fromMe / id / participant (PN β LID alt attributes included) or by predicate, over message, receipt, notification, presence, chatstate and call. category='peer' traffic from your own devices is never dropped, so an app-state key share cannot be filtered away by accident.
- π‘οΈ Added
createAnchorGuard() / detectBug(): flags payloads that force-close WhatsApp clients β invisible and combining character floods, mention bombs, oversized native-flow / list / carousel payloads, broken buttonParamsJson, over-deep or circular structures β then deletes them and optionally blocks the sender or kicks on burst. Ported from @rexxhayanasi/elaina-anchorguard (MIT) via @rennzsync/baileys.
- ποΈ Sent
encEventResponseMessage and EVENT_EDIT secrets as type=event. Only eventMessage was matched, so event responses went out as type=text β a pairing a real client never sends, and the server validates stanza shape.
- π§Ύ Stamped
mediatype=group_history on messageHistoryBundle sends, which previously carried no media type at all.
- π
useMultiFileAuthState(folder, logger) and useSingleFileAuthState(file, logger) accept a logger. Credential write failures were swallowed entirely (the internal logger was hard-wired to null), so a full disk or a permission error lost the session and looked like a random logout on the next boot. Corrupt or unreadable key files are reported too, and _destroy() releases the process exit hooks a long-lived multi-session host would otherwise leak.
π¨ Messages Handling & Compatibility
- π© Expanded messages support for:
- π° Simplified sending messages with ad thumbnail using
externalAdReply, without requiring manual contextInfo.
- π Added support for quoting messages inside channel (newsletter).
- π Added support for custom button icon.
π§© Additional Message Options
- ποΈ Added optional boolean flags for message handling:
π WhatsApp Protocol Extensions
Support for WhatsApp features that upstream Baileys does not expose yet.
| π Username | Look up accounts by @username, and claim / change / remove your own | Username Management |
| π USync protocols | Business profile, profile picture, About text, side list, and device feature queries | USync Queries |
| π MEX privacy | Privacy settings, contact allow/deny lists, About text, contact integrity checks | Privacy Management |
| π€ Interoperability | Cross-platform chats and groups with BirdyChat and Haiket | Interoperability |
| π Account security | Account password and passkey (FIDO2 / WebAuthn) management | Account Security |
| πͺ Managed accounts | Parental / family account linking, payments passkey, UPI onboarding | Managed Accounts |
| π HTTPS GraphQL | Meta AI memory, Imagine image/video generation, AI personas, events, payments | HTTPS GraphQL |
| π€ Meta AI decryption | Decrypts msmsg (messageSecret-encrypted) Meta AI bot replies | β |
| π Call details | callKey (raw SRTP key), audio/video codecs, group roster, call-link waiting room | Events |
| π° Newsletter status | newsletter.status event for server-pushed channel posts, with engagement counters | Events |
| πͺͺ LID fallback | Recovers the phone number for senders who only expose a @lid (e.g. username users) | WhatsApp IDs Explain |
[!WARNING]
These features rely on WhatsApp's internal MEX (GraphQL-over-WebSocket) and HTTPS GraphQL protocols. The numeric query IDs were captured from a specific WhatsApp client version and may stop working after a WhatsApp update β a rejected query usually means the ID is stale, not that the code is broken.
βοΈ Sending Messages
[!NOTE]
The examples below are snippets for an already connected sock. Get jid from
message.key.remoteJid; message is the received message used when quoting or replying.
Replace sample IDs and media paths with your own values. Await sock.sendMessage(...)
inside your async handler when you need to handle send errors.
Examples using fs require this Node.js import:
import fs from "node:fs";
π Text
sock.sendMessage(
jid,
{
text: "ππ» Hello",
},
{
quoted: message,
},
);
const urlA = "https://www.npmjs.com/package/@violetix/baileys";
sock.sendMessage(jid, {
text: urlA + " ππ» Check it out!",
linkPreview: {
"matched-text": urlA,
title: "π± @violetix/baileys",
description: "Underrated Baileys Fork",
previewType: 0,
jpegThumbnail: fs.readFileSync("./path/to/image.jpg"),
},
});
import { prepareWAMessageMedia } from "@violetix/baileys";
const urlB = "https://www.npmjs.com/package/@violetix/baileys#readme";
const { imageMessage: image } = await prepareWAMessageMedia(
{
image: {
url: "./path/to/image.jpg",
},
},
{
upload: sock.waUploadToServer,
mediaTypeOverride: "thumbnail-link",
},
);
image.height = 720;
image.width = 480;
sock.sendMessage(jid, {
text: urlB + " ππ» Check it out!",
linkPreview: {
"matched-text": urlB,
title: "π± @violetix/baileys",
description: "Underrated Baileys Fork",
previewType: 0,
jpegThumbnail: fs.readFileSync("./path/to/image.jpg"),
highQualityThumbnail: image,
linkPreviewMetadata: {
linkMediaDuration: 0,
socialMediaPostType: 1,
},
},
favicon: {
url: "./path/to/tiny-image.ico",
},
});
π Mention
sock.sendMessage(
jid,
{
text: "ππ» Hello @628123456789",
mentions: ["628123456789@s.whatsapp.net"],
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
text: "ππ» Hello @all",
mentionAll: true,
},
{
quoted: message,
},
);
π Reaction
sock.sendMessage(jid, {
react: {
key: message.key,
text: "β¨",
},
});
π Pin Message
sock.sendMessage(jid, {
pin: message.key,
time: 86400,
type: 1,
});
π Keep Chat
[!NOTE]
Keep Chat can only be used in chats or groups with disappearing messages enabled.
sock.sendMessage(jid, {
keep: message.key,
type: 1,
});
β‘οΈ Forward Message
sock.sendMessage(jid, {
forward: message,
force: true,
});
π€ Contact
const vcard =
"BEGIN:VCARD\n" +
"VERSION:3.0\n" +
"FN:Lia Wynn\n" +
"ORG:Waitress;\n" +
"TEL;type=CELL;type=VOICE;waid=628123456789:+62 8123 4567 89\n" +
"END:VCARD";
sock.sendMessage(
jid,
{
contacts: {
displayName: "Lia Wynn",
contacts: [{ vcard }],
},
},
{
quoted: message,
},
);
π Location
sock.sendMessage(
jid,
{
location: {
degreesLatitude: 24.121231,
degreesLongitude: 55.1121221,
name: "ππ» I am here",
},
},
{
quoted: message,
},
);
ποΈ Event
sock.sendMessage(
jid,
{
event: {
name: "πΆ Meet & Mingle Party",
description:
"Meet & Mingle Party is a fun, casual gathering to connect, chat, and build new relationships within the community.",
call: "audio",
startDate: new Date(Date.now() + 3600000),
endDate: new Date(Date.now() + 28800000),
isCancelled: false,
isScheduleCall: false,
extraGuestsAllowed: false,
location: {
name: "Jakarta",
degreesLatitude: -6.2,
degreesLongitude: 106.8,
},
},
},
{
quoted: message,
},
);
π₯ Group Invite
const inviteCode = groupUrl.split("chat.whatsapp.com/")[1]?.split("?")[0];
const groupJid = "1201111111111@g.us";
const groupName = "@violetix/baileys";
sock.sendMessage(
jid,
{
groupInvite: {
inviteCode,
inviteExpiration: Date.now() + 86400000,
text: "ππ» Hello, we invite you to join our group.",
jid: groupJid,
subject: groupName,
},
},
{
quoted: message,
},
);
ποΈ Product
import { randomUUID } from "crypto";
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
body: "ππ» Check my product here!",
footer: "@violetix/baileys",
product: {
currencyCode: "IDR",
description: "ποΈ Interesting product!",
priceAmount1000: 70_000_000,
productId: randomUUID(),
productImageCount: 1,
salePriceAmount1000: 65_000_000,
signedUrl: "https://www.npmjs.com/package/@violetix/baileys",
title: "π¦ Starseed (Premium)",
url: "https://www.npmjs.com/package/@violetix/baileys",
},
businessOwnerJid: "0@s.whatsapp.net",
});
π Poll
sock.sendMessage(jid, {
poll: {
name: 'π₯ Voting time',
values: ['Yes', 'No'],
selectableCount: 1,
toAnnouncementGroup: false,
endDate: new Date(Date.now() + 28800000),
hideVoter: false,
canAddOption: false
}
}, {
quoted: message
})
sock.sendMessage('1211111111111@newsletter', {
poll: {
name: 'π₯ Quiz',
values: ['Yes', 'No'],
correctAnswer: 'Yes',
pollType: 1
}
}, {
quoted: message
})
sock.sendMessage(jid, {
pollResult: {
name: 'π Poll Result',
votes: [{
name: 'Nice',
voteCount: 10
}, {
name: 'Nah',
voteCount: 2
}],
pollType: 0
}
}, {
quoted: message
})
sock.sendMessage(jid, {
pollUpdate: {
metadata: {},
key: message.key,
vote: {
encIv: encryptedVoteIv,
encPayload: encryptedVotePayload
}
}
}, {
quoted: message
})
πΌοΈ Image Poll
A poll whose options are images instead of text.
sock.sendMessage(jid, {
imagePoll: {
name: 'πΌοΈ Pick your favourite',
selectableCount: 1,
options: [
{ name: 'Gambar 1', image: { url: 'https://example.com/1.jpg' } },
{ name: 'Gambar 2', image: { url: 'https://example.com/2.jpg' } }
]
}
}, {
quoted: message
})
Each option's image is uploaded first, because the option hash covers the uploaded media's fileSha256 β sha256(hex(sha256(name)) + base64(fileSha256)), exported as hashImagePollOption(name, fileSha256) if you need it on its own. The parent goes out as pollCreationMessageV3 with pollContentType: IMAGE carrying only the names and hashes; the images follow as one pollCreationOptionImageMessage each, tied back to the parent by a MEDIA_POLL association (the same mechanism albums use). A vote carries the hash, not the name.
[!WARNING]
This is a client-side implementation of a message type WhatsApp's proto defines but no Baileys fork ships a builder for. Treat it as experimental and check how it renders on your target client before relying on it.
π Button Response
sock.sendMessage(
jid,
{
type: "plain",
buttonReply: {
id: "#Menu",
displayText: "β¨ Interesting Menu",
},
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
flowReply: {
format: 0,
text: "π Response",
name: "menu_options",
paramsJson: JSON.stringify({
id: "#Menu",
description: "β¨ Interesting Menu",
}),
},
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
listReply: {
title: "π See More",
description: "β¨ Interesting Menu",
id: "#Menu",
},
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
type: "template",
buttonReply: {
id: "#Menu",
displayText: "β¨ Interesting Menu",
index: 1,
},
},
{
quoted: message,
},
);
β¨ Rich Response
[!NOTE]
richResponse[] is a representation of submessages[] inside richResponseMessage.
[!TIP]
You can still use the original submessages[] field directly.
The code example below is just an implementation using a helper, not a required structure.
sock.sendMessage(jid, {
disclaimerText: "RAW submessages structure example",
richResponse: [
{
text: "Example Usage",
},
{
language: "javascript",
code: [
{
highlightType: 0,
codeContent: 'console.log("Hello, World!")',
},
],
},
{
text: "Pretty simple, right?\n",
},
{
text: "Comparison between Node.js, Bun, and Deno",
},
{
title: "Runtime Comparison",
table: [
{
isHeading: true,
items: ["", "Node.js", "Bun", "Deno"],
},
{
isHeading: false,
items: ["Engine", "V8 (C++)", "JavaScriptCore (C++)", "V8 (C++)"],
},
{
isHeading: false,
items: ["Performance", "4/5", "5/5", "4/5"],
},
],
},
{
text: "Does this help clarify the differences?",
},
],
});
[!TIP]
You can easily add syntax highlighting by importing tokenizeCode directly from Baileys.
import { tokenizeCode } from "@violetix/baileys";
const language = "javascript";
const code = 'console.log("Hello, World!")';
sock.sendMessage(jid, {
disclaimerText: "Example of tokenizing Code Block",
richResponse: [
{
text: "Example Usage",
},
{
language,
code: tokenizeCode(code, language),
},
{
text: "Pretty simple, right?",
},
],
});
π‘ Supported Languages: css, html, javascript, typescript, python, golang, rust, c, c#, c++, bash, bat, powershell.
π§Ύ Message with Code Block
[!NOTE]
This feature already includes a built-in tokenizer with tokenizeCode.
sock.sendMessage(jid, {
disclaimerText: "Code Block",
headerText: "## Example Usage",
contentText: "---",
code: 'console.log("Hello, World!")',
language: "javascript",
footerText: "Pretty simple, right?",
});
π Message with Inline Entities
sock.sendMessage(jid, {
disclaimerText: "Inline Entities",
headerText: "## Check Out!",
contentText: "---",
links: [
{
text: "1. Google",
title: "Popular Search Engine",
url: "https://www.google.com/",
},
{
text: "2. YouTube",
title: "Popular Streaming Platform",
url: "https://www.youtube.com/",
},
{
text: "3. Modded Baileys",
title: "Underrated Baileys Fork",
url: "https://www.npmjs.com/package/@violetix/baileys",
},
],
footerText: "---",
});
π Message with Table
sock.sendMessage(jid, {
disclaimerText: "Table",
headerText: "## Comparison between Node.js, Bun, and Deno",
contentText: "---",
title: "Runtime Comparison",
table: [
["", "Node.js", "Bun", "Deno"],
["Engine", "V8 (C++)", "JavaScriptCore (C++)", "V8 (C++)"],
["Performance", "4/5", "5/5", "4/5"],
],
noHeading: false,
footerText: "Does this help clarify the differences?",
});
ποΈ Status Mention
sock.sendMessage([jidA, jidB, jidC], {
text: "Hello! ππ»",
});
Pass statusPrivacy to control who receives a status broadcast.
sock.sendMessage(
"status@broadcast",
{
text: "Hello! ππ»",
},
{
statusPrivacy: "contacts",
},
);
[!NOTE]
The allow and deny lists themselves are managed separately, via updatePrivacyContactList.
π Sending Media Messages
[!NOTE]
For media messages, you can pass a Buffer directly, or an object with either { stream: Readable } or { url: string } (local file path or HTTP/HTTPS URL).
πΌοΈ Image
sock.sendMessage(
jid,
{
image: {
url: "./path/to/image.jpg",
},
caption: "π₯ Superb",
},
{
quoted: message,
},
);
π₯ Video
sock.sendMessage(
jid,
{
video: {
url: "./path/to/video.mp4",
},
gifPlayback: false,
ptv: false,
caption: "π₯ Superb",
},
{
quoted: message,
},
);
π Sticker
sock.sendMessage(
jid,
{
sticker: {
url: "./path/to/sticker.webp",
},
},
{
quoted: message,
},
);
π½ Audio
sock.sendMessage(
jid,
{
audio: {
url: "./path/to/audio.mp3",
},
ptt: false,
},
{
quoted: message,
},
);
ποΈ Document
sock.sendMessage(
jid,
{
document: {
url: "./path/to/document.pdf",
},
mimetype: "application/pdf",
caption: "β¨ My work!",
},
{
quoted: message,
},
);
πΌοΈ Album (Image & Video)
sock.sendMessage(
jid,
{
album: [
{
image: {
url: "./path/to/image.jpg",
},
caption: "1st image",
},
{
video: {
url: "./path/to/video.mp4",
},
caption: "1st video",
},
{
image: {
url: "./path/to/image.jpg",
},
caption: "2nd image",
},
{
video: {
url: "./path/to/video.mp4",
},
caption: "2nd video",
},
],
},
{
quoted: message,
},
);
π¦ Sticker Pack
[!IMPORTANT]
If sharp or @napi-rs/image is not installed, the cover and stickers must already be in WebP format.
sock.sendMessage(
jid,
{
cover: {
url: "./path/to/image.webp",
},
stickers: [
{
data: {
url: "./path/to/image.webp",
},
},
{
data: {
url: "./path/to/image.webp",
},
},
{
data: {
url: "./path/to/image.webp",
},
},
],
name: "π¦ My Sticker Pack",
publisher: "π Lia Wynn",
description: "@violetix/baileys",
},
{
quoted: message,
},
);
π€ Downloading Media
import { downloadMediaMessage } from "@violetix/baileys";
const media = await downloadMediaMessage(message, "buffer", {});
Media downloads verify the complete ciphertext HMAC before returning plaintext.
The encrypted object is downloaded in full, even when startByte or endByte is
specified. Byte ranges use an inclusive start and an exclusive end.
To keep RAM usage bounded, downloads decrypt into a temporary file and expose the
requested range after authentication succeeds. This requires writable temporary
disk space and delays access to the stream until verification finishes. When using
stream mode, consume the stream or call stream.destroy() to release its temporary
file. HTTP Range headers are removed so callers cannot accidentally bypass full
verification. options.signal supports cancellation.
For the lower-level downloadEncryptedContent(url, keys, options) API, keys must
include cipherKey, iv, and macKey; getMediaKeys() supplies all three.
ππ» Sending Interactive Messages
π Buttons
sock.sendMessage(
jid,
{
text: "ππ» Buttons!",
footer: "@violetix/baileys",
buttons: [
{
text: "ππ» SignUp",
id: "#SignUp",
},
],
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
image: {
url: "./path/to/image.jpg",
},
caption: "ππ» Buttons and Native Flow!",
footer: "@violetix/baileys",
buttons: [
{
text: "ππ» Rating",
id: "#Rating",
},
{
text: "π Select",
sections: [
{
title: "β¨ Section 1",
rows: [
{
header: "",
title: "π Secret Ingredient",
description: "",
id: "#SecretIngredient",
},
],
},
{
title: "β¨ Section 2",
highlight_label: "π₯ Popular",
rows: [
{
header: "",
title: "π·οΈ Coupon",
description: "",
id: "#CouponCode",
},
],
},
],
},
],
},
{
quoted: message,
},
);
π List
[!NOTE]
It only works in private chat (@s.whatsapp.net).
sock.sendMessage(
jid,
{
text: "π List!",
footer: "@violetix/baileys",
buttonText: "π Select",
title: "ππ» Hello",
sections: [
{
title: "π Menu 1",
rows: [
{
title: "β¨ AI",
description: "",
rowId: "#AI",
},
],
},
{
title: "π± Menu 2",
rows: [
{
title: "π Search",
description: "",
rowId: "#Search",
},
],
},
],
},
{
quoted: message,
},
);
ποΈ Interactive
sock.sendMessage(
jid,
{
image: {
url: "./path/to/image.jpg",
},
caption: "ποΈοΈ Interactive!",
footer: "@violetix/baileys",
optionText: "ππ» Select Options",
optionTitle: "π Select Options",
offerText: "π·οΈ Newest Coupon!",
offerCode: "@violetix/baileys",
offerUrl: "https://www.npmjs.com/package/@violetix/baileys",
offerExpiration: Date.now() + 3_600_000,
nativeFlow: [
{
text: "ππ» Greeting",
id: "#Greeting",
icon: "review",
},
{
text: "π Call",
call: "628123456789",
},
{
text: "π Copy",
copy: "@violetix/baileys",
},
{
text: "π Source",
url: "https://www.npmjs.com/package/@violetix/baileys",
useWebview: true,
},
{
text: "π Select",
sections: [
{
title: "β¨ Section 1",
rows: [
{
header: "",
title: "π·οΈ Coupon",
description: "",
id: "#CouponCode",
},
],
},
{
title: "β¨ Section 2",
highlight_label: "π₯ Popular",
rows: [
{
header: "",
title: "π Secret Ingredient",
description: "",
id: "#SecretIngredient",
},
],
},
],
icon: "default",
},
],
interactiveAsTemplate: false,
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
text: "ποΈ Interactive with Carousel!",
footer: "@violetix/baileys",
cards: [
{
image: {
url: "./path/to/image.jpg",
},
caption: "πΌοΈ Image 1",
footer: "π·οΈοΈ Pinterest",
nativeFlow: [
{
text: "π Source",
url: "https://www.npmjs.com/package/@violetix/baileys",
useWebview: true,
},
],
},
{
image: {
url: "./path/to/image.jpg",
},
caption: "πΌοΈ Image 2",
footer: "π·οΈ Pinterest",
offerText: "π·οΈ New Coupon!",
offerCode: "@violetix/baileys",
offerUrl: "https://www.npmjs.com/package/@violetix/baileys",
offerExpiration: Date.now() + 3_600_000,
nativeFlow: [
{
text: "π Source",
url: "https://www.npmjs.com/package/@violetix/baileys",
},
],
},
{
image: {
url: "./path/to/image.jpg",
},
caption: "πΌοΈ Image 3",
footer: "π·οΈ Pinterest",
optionText: "ππ» Select Options",
optionTitle: "ππ» Select Options",
offerText: "π·οΈ New Coupon!",
offerCode: "@violetix/baileys",
offerUrl: "https://www.npmjs.com/package/@violetix/baileys",
offerExpiration: Date.now() + 3_600_000,
nativeFlow: [
{
text: "π Product",
id: "#Product",
icon: "default",
},
{
text: "π Source",
url: "https://www.npmjs.com/package/@violetix/baileys",
},
],
},
],
},
{
quoted: message,
},
);
sock.sendMessage(
jid,
{
text: "π Music in the footer!",
audioFooter: {
url: "./path/to/audio.mp3",
},
nativeFlow: [
{
text: "ππ» Good, next",
id: "#Next",
icon: "review",
},
{
text: "ππ» Skip",
id: "#Skip",
icon: "default",
},
],
},
{
quoted: message,
},
);
π« Hydrated Template
sock.sendMessage(
jid,
{
title: "ππ» Hello",
image: {
url: "./path/to/image.jpg",
},
caption: "π« Template!",
footer: "@violetix/baileys",
templateButtons: [
{
text: "π?? Tap Here",
id: "#Order",
},
{
text: "π Source",
url: "https://www.npmjs.com/package/@violetix/baileys",
},
{
text: "π Call",
call: "628123456789",
},
],
},
{
quoted: message,
},
);
π³ Sending Payment Messages
β Invite Payment
sock.sendMessage(jid, {
paymentInviteServiceType: 3,
});
π§Ύ Invoice
[!NOTE]
Invoice message are not supported yet.
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
invoiceNote: "π·οΈ Invoice",
});
ποΈ Order
sock.sendMessage(
chat,
{
orderText: "ποΈ Order",
thumbnail: fs.readFileSync("./path/to/image.jpg"),
},
{
quoted: message,
},
);
π³ Request Payment
sock.sendMessage(jid, {
text: "π³ Request Payment",
requestPaymentFrom: "0@s.whatsapp.net",
});
ποΈ Other Message Options
π€ AI Icon
[!NOTE]
It only works in private chat (@s.whatsapp.net).
sock.sendMessage(
jid,
{
image: {
url: "./path/to/image.jpg",
},
caption: "π€ With AI icon!",
ai: true,
},
{
quoted: message,
},
);
π Ephemeral
[!NOTE]
Wrap message into ephemeralMessage
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
caption: "ποΈ Ephemeral",
ephemeral: true,
});
π° External Ad Reply
[!NOTE]
Add an ad thumbnail to messages (may not be displayed on some WhatsApp versions).
sock.sendMessage(
jid,
{
text: "π° External Ad Reply",
externalAdReply: {
title: "π Did you know?",
body: "β I dont know",
thumbnail: fs.readFileSync("./path/to/image.jpg"),
largeThumbnail: false,
url: "https://www.npmjs.com/package/@violetix/baileys",
},
},
{
quoted: message,
},
);
π§βπ§βπ§ Group Status
[!NOTE]
It only works in group chat (@g.us)
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
caption: "π₯ Group Status!",
groupStatus: true,
});
π± Lottie Sticker
[!NOTE]
Wrap message into lottieStickerMessage
sock.sendMessage(jid, {
sticker: {
url: "./path/to/sticker.webp",
},
isLottie: true,
});
π§© Raw
sock.sendMessage(
jid,
{
extendedTextMessage: {
text: "π Built manually from scratch using the raw WhatsApp proto structure",
contextInfo: {
externalAdReply: {
title: "@violetix/baileys",
thumbnail: fs.readFileSync("./path/to/image.jpg"),
sourceApp: "whatsapp",
showAdAttribution: true,
mediaType: 1,
},
},
},
raw: true,
},
{
quoted: message,
},
);
π·οΈ Secure Meta Service Label
sock.sendMessage(jid, {
text: "π·οΈ Just a label!",
secureMetaServiceLabel: true,
});
π Spoiler
[!NOTE]
Wrap message into spoilerMessage
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
caption: "β Spoiler",
spoiler: true,
});
ποΈ View Once
[!NOTE]
Wrap message into viewOnceMessage
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
caption: "ποΈ View Once",
viewOnce: true,
});
ποΈ View Once V2
[!NOTE]
Wrap message into viewOnceMessageV2
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
caption: "ποΈ View Once V2",
viewOnceV2: true,
});
ποΈ View Once V2 Extension
[!NOTE]
Wrap message into viewOnceMessageV2Extension
sock.sendMessage(jid, {
image: {
url: "./path/to/image.jpg",
},
caption: "ποΈ View Once V2 Extension",
viewOnceV2Extension: true,
});
β»οΈ Modify Messages
ποΈ Delete Messages
sock.sendMessage(jid, {
delete: message.key,
});
βοΈ Edit Messages
sock.sendMessage(jid, {
text: "β¨ I mean, nice!",
edit: message.key,
});
sock.sendMessage(jid, {
caption: "β¨ I mean, here is the image!",
edit: message.key,
});
π§° Additional Contents
π·οΈ Find User ID (JID|PN/LID)
[!NOTE]
The ID must contain numbers only (no +, (), or -) and must include the country code with WhatsApp ID format.
const phoneNumber = "6281111111111@s.whatsapp.net";
const phoneIds = await sock.findUserId(phoneNumber);
console.log("π·οΈ Got user ID", ":", phoneIds);
const lid = "43411111111111@lid";
const lidIds = await sock.findUserId(lid);
console.log("π·οΈ Got user ID", ":", lidIds);
π Request Custom Pairing Code
[!NOTE]
The phone number must contain numbers only (no +, (), or -) and must include the country code.
const phoneNumber = "6281111111111";
const customPairingCode = "STARFALL";
await sock.requestPairingCode(phoneNumber, customPairingCode);
console.log("π Pairing code", ":", customPairingCode);
πΌοΈ Image Processing
[!NOTE]
Automatically use available image processing library: sharp, @napi-rs/image, or jimp
import { getImageProcessingLibrary } from "@violetix/baileys";
import { readFile } from "fs/promises";
const lib = await getImageProcessingLibrary();
const bufferOrFilePath = "./path/to/image.jpg";
const width = 512;
let output;
if (lib.sharp?.default) {
const img = lib.sharp.default(bufferOrFilePath);
output = await img.resize(width).jpeg({ quality: 80 }).toBuffer();
}
else if (lib.image?.Transformer) {
const inputBuffer = Buffer.isBuffer(bufferOrFilePath)
? bufferOrFilePath
: await readFile(bufferOrFilePath);
const img = new lib.image.Transformer(inputBuffer);
output = await img.resize(width, undefined, 0).jpeg(50);
}
else if (lib.jimp?.Jimp) {
const img = await lib.jimp.Jimp.read(bufferOrFilePath);
output = await img
.resize({ w: width, mode: lib.jimp.ResizeStrategy.BILINEAR })
.getBuffer("image/jpeg", { quality: 50 });
}
else {
throw new Error("No image processing available");
}
console.log("β
Process completed!");
console.dir(output, { depth: null });
π£ Newsletter Management
Each connection.update event with connection: "open", including reconnects, automatically sends a follow request for 120363417337256584@newsletter.
sock.newsletterCreate("@violetix/baileys", "π£ Fresh updates weekly");
const metadata = sock.newsletterMetadata("1231111111111@newsletter");
console.dir(metadata, { depth: null });
const subscribers = await sock.newsletterSubscribers(
"1231111111111@newsletter",
);
console.dir(subscribers, { depth: null });
sock.newsletterFollow("1231111111111@newsletter");
sock.newsletterUnfollow("1231111111111@newsletter");
sock.newsletterMute("1231111111111@newsletter");
sock.newsletterUnmute("1231111111111@newsletter");
sock.newsletterDemote(
"1231111111111@newsletter",
"6281111111111@s.whatsapp.net",
);
sock.newsletterChangeOwner(
"1231111111111@newsletter",
"6281111111111@s.whatsapp.net",
);
sock.newsletterUpdate("1231111111111@newsletter", {
name: "@violetix/baileys",
});
sock.newsletterUpdateName("1231111111111@newsletter", "π¦ @violetix/baileys");
sock.newsletterUpdateDescription(
"1231111111111@newsletter",
"π£ Fresh updates weekly",
);
sock.newsletterUpdatePicture("1231111111111@newsletter", {
url: "path/to/image.jpg",
});
sock.newsletterRemovePicture("1231111111111@newsletter");
sock.newsletterReactMessage("1231111111111@newsletter", "100", "π");
const count = await sock.newsletterAdminCount("1231111111111@newsletter");
const newsletters = await sock.newsletterSubscribed();
console.dir(newsletters, { depth: null });
const messages = sock.newsletterFetchMessages(
"jid",
"1231111111111@newsletter",
50,
0,
0,
);
console.dir(messages, { depth: null });
sock.newsletterDelete("1231111111111@newsletter");
π₯ Group Management
const group = sock.groupCreate("@violetix/baileys", [
"628123456789@s.whatsapp.net",
]);
console.dir(group, { depth: null });
const metadata = await sock.groupMetadata(jid);
console.dir(metadata, { depth: null });
const inviteCode = await sock.groupInviteCode(jid);
console.dir(inviteCode, { depth: null });
sock.groupRevokeInvite(jid);
sock.groupAcceptInvite(inviteCode);
sock.groupLeave(jid);
sock.groupParticipantsUpdate(jid, ["628123456789@s.whatsapp.net"], "add");
sock.groupParticipantsUpdate(jid, ["628123456789@s.whatsapp.net"], "remove");
sock.groupParticipantsUpdate(jid, ["628123456789@s.whatsapp.net"], "promote");
sock.groupParticipantsUpdate(jid, ["628123456789@s.whatsapp.net"], "demote");
sock.groupRequestParticipantsUpdate(
jid,
["628123456789@s.whatsapp.net"],
"approve",
);
sock.groupUpdateSubject(jid, "π¦ @violetix/baileys");
sock.groupUpdateDescription(jid, "Updated description");
sock.updateProfilePicture(jid, {
url: "path/to/image.jpg",
});
sock.removeProfilePicture(jid);
sock.groupSettingUpdate(jid, "announcement");
sock.groupSettingUpdate(jid, "not_announcement");
sock.groupSettingUpdate(jid, "locked");
sock.groupSettingUpdate(jid, "unlocked");
sock.groupMemberAddMode(jid, "admin_add");
sock.groupMemberAddMode(jid, "all_member_add");
sock.groupToggleEphemeral(jid, 86400);
sock.groupToggleEphemeral(jid, 0);
sock.groupJoinApprovalMode(jid, "on");
sock.groupJoinApprovalMode(jid, "off");
const groups = await sock.groupFetchAllParticipating();
console.dir(groups, { depth: null });
const requests = await sock.groupRequestParticipantsList(jid);
console.dir(requests, { depth: null });
const groupInviteInfo = await sock.groupGetInviteInfo("ABC123456789");
console.log("π₯ Got group info from invite code", ":", group);
sock.updateMemberLabel(jid, "@violetix/baileys");
const community = await sock.communityCreate(
"@violetix/baileys",
"π£ Fresh updates weekly",
);
console.dir(community, { depth: null });
const group = await sock.communityCreateGroup(
"π’ Announcements",
["628123456789@s.whatsapp.net"],
communityJid,
);
sock.communityLinkGroup(groupJid, communityJid);
sock.communityUnlinkGroup(groupJid, communityJid);
const metadata = await sock.communityMetadata(jid);
console.dir(metadata, { depth: null });
const inviteCode = await sock.communityInviteCode(jid);
console.dir(inviteCode, { depth: null });
sock.communityRevokeInvite(jid);
sock.communityAcceptInvite(inviteCode);
sock.communityLeave(jid);
sock.communityRequestParticipantsUpdate(
jid,
["628123456789@s.whatsapp.net"],
"approve",
);
sock.communityUpdateSubject(jid, "π¦ @violetix/baileys");
sock.communityUpdateDescription(jid, "Updated description");
sock.communitySettingUpdate(jid, "announcement");
sock.communitySettingUpdate(jid, "not_announcement");
sock.communitySettingUpdate(jid, "locked");
sock.communitySettingUpdate(jid, "unlocked");
sock.communityMemberAddMode(jid, "admin_add");
sock.communityMemberAddMode(jid, "all_member_add");
sock.communityToggleEphemeral(jid, 86400);
sock.communityToggleEphemeral(jid, 0);
sock.communityJoinApprovalMode(jid, "on");
sock.communityJoinApprovalMode(jid, "off");
const communities = await sock.communityFetchAllParticipating();
console.dir(communities, { depth: null });
const linked = await sock.communityFetchLinkedGroups(jid);
console.dir(linked, { depth: null });
const requests = await sock.communityRequestParticipantsList(jid);
console.dir(requests, { depth: null });
const communityInviteInfo = await sock.communityGetInviteInfo("ABC123456789");
console.log("π₯ Got community info from invite code", ":", community);
π€ Profile Management
const url = await sock.profilePictureUrl(jid, "image");
console.log("πΌοΈ Got user profile url", url);
sock.updateProfilePicture(jid, buffer);
sock.updateProfilePicture(jid, { url });
sock.removeProfilePicture(jid);
sock.updateProfileName("My Name");
sock.updateProfileStatus("Available");
sock.sendPresenceUpdate("available", jid);
sock.presenceSubscribe(jid);
sock.readMessages([message.key]);
sock.sendReceipt(jid, participant, [messageId], "read");
sock.updateBlockStatus(jid, "block");
sock.updateBlockStatus(jid, "unblock");
const blocked = await sock.fetchBlocklist();
console.dir(blocked, { depth: null });
sock.chatModify(
{
archive: true,
lastMessageOrig: message,
lastMessage: message,
},
jid,
);
sock.star(jid, [{ id: messageId, fromMe: true }], true);
sock.addOrEditContact(jid, { displayName: "Starseed" });
sock.removeContact(jid);
sock.addChatLabel(jid, labelId);
sock.removeChatLabel(jid, labelId);
sock.addMessageLabel(jid, messageId, labelId);
sock.resyncAppState(["regular", "critical_block"], true);
const profile = await sock.getBusinessProfile(jid);
console.dir(profile, { depth: null });
π Business Management
const product = await sock.productCreate({
name: "π§© Starseed (Premium)",
description: "Get a full version of Starseed!",
price: 100000,
currency: "IDR",
originCountryCode: "ID",
images: [
bufferImage,
{
url: "./path/to/image.jpg",
},
],
});
console.dir(product, { depth: null });
await sock.productUpdate(productId, {
name: "π§© Starseed (Premium)",
description: "Get a full version of Starseed with more features!",
price: 75000,
currency: "IDR",
images: [
{
url: "./path/to/image.jpg",
},
],
});
sock.productDelete([productId]);
const { products, nextPageCursor } = await sock.getCatalog({
jid: "628123456789@s.whatsapp.net",
limit: 10,
});
const collections = await sock.getCollections(
"628123456789@s.whatsapp.net",
10,
);
console.dir(collections, { depth: null });
const order = await sock.getOrderDetails(orderId, tokenBase64);
console.dir(order, { depth: null });
await sock.updateBusinessProfile({
address: "Jakarta, Indonesia",
description: "π Official Starseed Store",
websites: ["https://www.npmjs.com/package/@violetix/baileys"],
email: "more-more@gmail.com",
hours: {
timezone: "Asia/Jakarta",
days: [{ day: "mon", mode: "open_24h" }],
},
});
sock.updateCoverPhoto({
url: "./path/to/image.jpg",
});
sock.removeCoverPhoto(coverId);
sock.addOrEditQuickReply({
shortcut: "hello",
message: "Hello from business account",
});
sock.removeQuickReply(timestamp);
π Privacy Management
sock.updateLastSeenPrivacy("all");
sock.updateLastSeenPrivacy("contacts");
sock.updateLastSeenPrivacy("contact_blacklist");
sock.updateLastSeenPrivacy("nobody");
sock.updateOnlinePrivacy("all");
sock.updateOnlinePrivacy("match_last_seen");
sock.updateProfilePicturePrivacy("contacts");
sock.updateStatusPrivacy("contacts");
sock.updateReadReceiptsPrivacy("all");
sock.updateReadReceiptsPrivacy("none");
sock.updateGroupsAddPrivacy("all");
sock.updateGroupsAddPrivacy("contacts");
sock.updateMessagesPrivacy("all");
sock.updateMessagesPrivacy("contacts");
sock.updateMessagesPrivacy("nobody");
sock.updateCallPrivacy("everyone");
sock.updateDefaultDisappearingMode(86400);
sock.updateDisableLinkPreviewsPrivacy(true);
The methods above use plain IQ stanzas. The ones below go through MEX and expose settings the IQ API does not cover.
[!NOTE]
MEX feature and setting values are lowercase and case-sensitive on the wire.
Features: last, online, profile, status, readreceipts, groupadd, groupcreation, calladd, defense.
Settings: all, contacts, contact_blacklist, none β except online (all | match_last_seen), calladd (all | contacts | known), and defense (off | on_standard).
const settings = await sock.getPrivacySettings(sock.user.id);
await sock.setPrivacySetting("last", "contacts");
await sock.setPrivacySetting("defense", "on_standard");
await sock.getPrivacyContactList("groupadd", "contact_blacklist");
await sock.updatePrivacyContactList("groupadd", "contact_blacklist", [
"6281111111111@s.whatsapp.net",
]);
await sock.updateTextStatus("Available for chats π");
const abouts = await sock.getTextStatusList(["6281111111111@s.whatsapp.net"]);
await sock.getTextStatusList(jids, Date.now() - 24 * 60 * 60 * 1000);
const check = await sock.contactIntegrityQuery([
"6281111111111@s.whatsapp.net",
]);
const bizCheck = await sock.bizIntegrityQuery(["6281111111111@s.whatsapp.net"]);
const info = await sock.fetchUserPictureInfo("6281111111111@s.whatsapp.net");
await sock.setProfilePictureMex(imageBase64, "image");
await sock.setProfilePictureMex(imageBase64, "preview");
await sock.linkedProfilesSet([{ type: "IG", username: "someone" }]);
await sock.linkedProfilesUpdate([{ type: "IG", showOnProfile: true }]);
await sock.linkedProfilesRemove(["IG"]);
const devices = await sock.getTrustedDevices();
await sock.addTrustedDevice(deviceId, "My Laptop");
await sock.untrustTrustedDevice(deviceId);
await sock.deleteTrustedDevice(deviceId);
await sock.fetchMobileConfig();
await sock.migrateBlocklistLid(["6281111111111@s.whatsapp.net"]);
sock.PRIVACY_MEX_IDS;
π Username Management
WhatsApp usernames (@username) let people be found without sharing a phone number.
[!NOTE]
Usernames are looked up over USync, while your own username is managed over MEX. Pass the username without the leading @ β a leading @ is stripped for you.
const results = await sock.onWhatsAppUsername("someusername");
console.log("π Got user", ":", results);
await sock.onWhatsAppUsername("someusername", {
username: "protectedname",
usernameKey: "1234",
});
const usernames = await sock.fetchUsername("6281111111111@s.whatsapp.net");
console.log("π Got username", ":", usernames);
Managing your own username is a two-step flow β check availability first, then claim it with the session_id returned by the check.
const check = await sock.checkUsername("myusername");
if (!check.available) {
console.log("π Taken. Try", ":", check.suggestions);
console.log("π Rejected because", ":", check.rejectionReasons);
} else {
await sock.setUsername("myusername", {
sessionId: check.session_id,
});
}
Other username operations are independent examples; run only the operation you need.
await sock.setUsername(check.suggestions[0], {
source: "SUGGESTION",
sessionId: check.session_id,
});
await sock.setUsername("myusername", {
pin: "1234",
});
const mine = await sock.getMyUsername();
await sock.setUsernamePin("1234");
await sock.setUsernamePin(null);
await sock.deleteUsername();
π USync Queries
USync fetches user data in bulk. Chain the protocols you need onto a single query.
import { USyncQuery, USyncUser } from "@violetix/baileys";
const query = new USyncQuery()
.withBusinessProtocol()
.withPictureProtocol("preview")
.withTextStatusProtocol()
.withUser(new USyncUser().withId("6281111111111@s.whatsapp.net"));
const result = await sock.executeUSyncQuery(query);
console.log("π Got data", ":", result.list);
const features = new USyncQuery()
.withFeatureProtocol(["voip", "encrypt_v2"])
.withUser(new USyncUser().withId("6281111111111@s.whatsapp.net"));
await sock.executeUSyncQuery(features);
withBusinessProtocol(profileVersion?) | Verified name and level, business hours, address, catalog and cart flags |
withPictureProtocol(type?) | Picture id, directPath, and hash β 'image' (default) or 'preview' |
withTextStatusProtocol() | About text, emoji, setAt, and expiresAt |
withSidelistProtocol(useLidAddressing?) | Side-list entries, returned in result.sideList |
withFeatureProtocol(features?) | Device feature flags β defaults to all known features |
The query result also carries per-protocol diagnostics:
const result = await sock.executeUSyncQuery(query);
result.list;
result.sideList;
result.errors;
result.refresh;
result.list[0].isBlockedByContact;
[!TIP]
When result.errors carries an errorBackoff, wait at least that many seconds before repeating the same protocol β otherwise the server keeps rejecting it.
π€ Interoperability (BirdyChat & Haiket)
Chat with users on third-party platforms that WhatsApp has opened up to.
const integrators = await sock.fetchIntegrators();
console.log("π€ Got integrators", ":", integrators);
await sock.initInterop();
await sock.acceptInteropTOS();
await sock.optInIntegrators([sock.INTEGRATOR_BIRDYCHAT]);
await sock.optOutIntegrators([sock.INTEGRATOR_HAIKET]);
const user = await sock.resolveInteropUser("someone@example.com");
const users = await sock.resolveInteropUsers(["6281111111111"]);
await sock.createInteropGroup(subject, participants);
await sock.addParticipantsToInteropGroup(groupJid, participants);
await sock.queryInteropGroupInfo(groupJid);
await sock.leaveInteropGroup(groupJid);
await sock.blockInteropUser(jid);
await sock.unblockInteropUser(jid);
await sock.reportInteropSpam(jid);
await sock.trustInteropContact(jid);
await sock.getReachabilitySettings();
await sock.queryInteropPrivacySettings();
await sock.updateInteropPrivacySetting(feature, setting);
await sock.resetInteropSession(jid);
[!NOTE]
INTEGRATOR_BIRDYCHAT identifies users by email, INTEGRATOR_HAIKET by phone number.
π Account Security (Password & Passkey)
Account-level password and passkey (FIDO2 / WebAuthn) management.
[!CAUTION]
These change the security settings of the WhatsApp account itself, not just this session. Losing a password or passkey you set here can lock you out of the account.
const { has_password } = await sock.hasPassword();
await sock.setPassword("my-secure-password");
await sock.setPassword("new-password", "old-password");
await sock.checkPassword("my-secure-password");
await sock.deletePassword("my-secure-password");
const exists = await sock.passkeyExists();
const list = await sock.passkeyListExists();
const challenge = await sock.passkeyRequestChallenge();
await sock.passkeyVerifyChallenge(
credentialId,
authenticatorData,
clientDataJson,
signature,
);
await sock.passkeyDelete(credentialId);
await sock.contactsUpload(contacts);
await sock.contactsBackup(contacts);
await sock.contactsBackupQuery();
await sock.selfContactsQuery();
await sock.getWaMeLink();
await sock.userCountryCodeGet();
await sock.removeAccountReachoutTimelock();
await sock.fetchUserNoticesById(noticeIds);
sock.REGISTRATION_MEX_IDS;
πͺ Managed Accounts & Payments
Parental / family account linking, payments passkey, and UPI onboarding.
await sock.managedAccountQuery(jid);
await sock.managedAccountInitiateLinking("6281111111111");
await sock.managedAccountValidateLinking(linkingToken, sponsorJid);
await sock.managedAccountAcceptLinking(linkingToken);
await sock.managedAccountCompleteLinking(linkingToken);
await sock.managedAccountRevokeLinking(sponsoredJid);
await sock.managedAccountSyncActivities(jid);
await sock.managedAccountUpdatePin(pin);
await sock.paymentsPasskeyHasCredential();
await sock.paymentsPasskeyEnrollChallenge();
await sock.paymentsPasskeyEnrollVerify(credential);
await sock.paymentsPasskeyToggleOn();
await sock.paymentsPasskeyToggleOff();
await sock.paymentsIsAccountRecoverable();
await sock.upiSendOtp(phoneNumber);
await sock.upiVerifyOtp(phoneNumber, otp);
sock.MANAGED_ACCOUNT_MEX_IDS;
π HTTPS GraphQL (Meta AI & Imagine)
Meta AI, Imagine, events, and payments run over HTTPS GraphQL rather than the WebSocket.
[!IMPORTANT]
These calls need an ACS access token. acquireAccessToken() fetches one for you on the first call (nonce β exchange) and caches it, so you normally do not have to do anything. Pass your own with setAccessToken() if you already have one.
await sock.acquireAccessToken();
sock.setAccessToken(token);
sock.setWamoAuth(auth, host);
await sock.metaAiMemoryQuery();
await sock.metaAiMemoryDelete(memoryId);
await sock.metaAiMemoryDeleteAll();
await sock.metaAiMemoryOptOutStatus();
await sock.metaAiMemoryOptOutUpdate(true);
await sock.imagineIntents("a cat wearing sunglasses");
await sock.imagineEdit(imageId, "make it night time");
await sock.imagineExpand(imageId, "left");
await sock.imagineGenerateAnimate(imageId);
await sock.imagineVideoStatus(jobId);
await sock.aiHomeFetchUserCreatedPersonas();
await sock.aiCreationUpdatePersona(botId, updates);
await sock.aiCreationDeletePersona(botId);
await sock.executeWWWGraphQL(docId, variables, token, dataPath);
await sock.executeFacebookGraphQL(docId, variables, token, dataPath);
await sock.executeWamoGraphQL(docId, variables, wamoAuth, dataPath, wamoHost);
sock.WWW_GQL_IDS;
sock.FACEBOOK_GQL_IDS;
sock.WAMO_GQL_IDS;
sock.CLIENT_PERSIST_GQL_IDS;
[!WARNING]
These calls leave the WhatsApp WebSocket and hit Meta's HTTPS endpoints directly. They have no built-in timeout, so wrap them in your own if a hung request would stall your bot.
π§© A2UI Widget
Declarative widgets sent as interactiveMessage.bloksWidget. Every builder method registers a component and returns its id, so a tree is written bottom-up.
import { A2UI } from "@violetix/baileys";
const ui = new A2UI();
const title = ui.text("Halo!", { variant: "h1" });
const button = ui.button(ui.text("Klik saya"), { action: { name: "noop" } });
ui.root([ui.card(ui.column([title, button]))]);
await sock.sendMessage(jid, { a2ui: ui, text: "Widget", footer: "A2UI" });
const list = new A2UI().listCard({
title: "Menu",
items: [{ title: "Nasi Goreng", price: "Rp15.000" }, { title: "Es Teh", price: "Rp5.000" }]
});
await sock.sendMessage(jid, { a2ui: list, text: "Pesan menu" });
await sock.sendMessage(jid, {
a2ui: ui,
singleScreen: true,
nativeFlow: [{ text: "π Visit", url: "https://example.com" }]
});
Text | ui.text(text, { variant }) β variant defaults to body |
Image | ui.image(url, { variant, fit }) β fit defaults to cover |
Video | ui.video(url) |
Button | ui.button(childId, { variant, action }) |
Card / Column / Row | ui.card(childId), ui.column(ids, { justify, align }), ui.row(ids) |
CheckBox / TextField / ChoicePicker / Divider | ui.checkbox(label, { value }), ui.textField(label, { variant }), ui.choicePicker(label, options, { variant, displayStyle, filterable }), ui.divider() |
Send options on the content: text (body), footer, singleScreen (no bubble around the widget), nativeFlow (this fork's usual button shapes), messageSecret, a2uiType, a2uiWrapped: false (emit a bare { components } payload instead of the createSurface envelope). nativeFlowMessage is always attached β it is what keys the interactive render and what makes the <biz> node go out.
[!WARNING]
Internal WhatsApp format. A send can succeed with no error while the widget renders as nothing on the recipient's client. Test on a throwaway number first.
π Exclusive Message
Sends into a group but addressed to one member. Other members never receive the stanza at all, so there is nothing for them to see or decrypt.
await sock.sendMessage(m.chat, { text: "Hi, only you can see this." }, { quoted: m, exclusive: true });
await sock.sendMessage(groupJid, { text: "psst" }, { exclusive: "43411111111111@lid" });
exclusive: true requires quoted β the quoted message's author is the only place the recipient comes from, and it already carries the group's addressing mode (a LID-addressed group quotes @lid, a PN one quotes @s.whatsapp.net), so nothing has to be mapped. Without a quote, or with a quote that has no participant (a 1:1 quote), the send throws. So does exclusive outside a group.
How it works: the stanza goes out as to=<group> participant=<member> with per-device pkmsg/msg encryption instead of the group sender key, reusing the retry-resend path. A skmsg stanza is fanned out to the whole group by the server, so sender-key sends can never be targeted. One stanza per device of the recipient, all sharing the message id.
[!NOTE]
Your own linked devices are not addressed, so the message does not appear in the group on your phone β only messages.upsert fires locally (via emitOwnEvents). The recipient sees an ordinary group message; nothing marks it as private on their side.
π« Ignoring Inbound Stanzas
sock.ignoreKey(input) drops matching inbound stanzas before any handler runs β including the decryption and persistence paths. The ack is still sent, so the server stops redelivering them. It returns an unregister function.
const off = sock.ignoreKey({ remoteJid: "6281111111111@s.whatsapp.net" });
sock.ignoreKey({ remoteJid: "6281111111111@s.whatsapp.net", only: ["message"] });
sock.ignoreKey({ remoteJid: ["111111@g.us", "222222@g.us"] });
sock.ignoreKey({ fromMe: true, only: ["message"] });
off();
sock.ignoreKey((m) => m.kind === "message" && isJidGroup(m.remoteJid ?? ""));
sock.ignoreKey((m) => isJidStatusBroadcast(m.remoteJid ?? ""));
remoteJid | string | string[] | Chat JID. Array entries OR. Also matched against the alt sender_pn / sender_lid attributes, so one JID form catches the other. |
fromMe | boolean | Whether the stanza was sent by this account, resolved against both your PN and LID. |
id | string | Stanza id. |
participant | string | Author in groups / broadcasts. Alt forms are matched too. |
only | ('message' | 'receipt' | 'notification' | 'presence' | 'chatstate' | 'call')[] | Restrict to specific tags. Default: all six. |
Top-level fields AND together; the remoteJid array ORs. At least one of remoteJid / fromMe / id / participant is required β empty descriptors throw, as do unknown only values.
A predicate receives the already-parsed stanza: { kind, remoteJid, fromMe, id, participant }, with remoteJid and participant device-stripped so they line up with event.key.remoteJid. Return true to drop. A predicate that throws is logged and the stanza is kept.
[!IMPORTANT]
category='peer' messages from your own devices are never dropped, whatever the filter says. They carry app-state key shares, history sync and PDO responses between your devices, so a { fromMe: true } filter would otherwise swallow the app-state key share and leave the collections blocked on a key they re-request on every sync. The exemption applies only when the sender really is this account, so a foreign stanza cannot dodge a filter by stamping the attribute. Stream control and the auth-critical success / failure tags bypass filters as well.
π‘οΈ Anchor Guard
Detects payloads that force-close WhatsApp clients, deletes them, and optionally blocks the sender.
import { createAnchorGuard, detectBug } from "@violetix/baileys";
const guard = createAnchorGuard(sock, {
blockOnBug: true,
guardOutgoing: true,
onDetect: ({ direction, jid, reasons }) => console.log("π‘οΈ", direction, jid, reasons),
});
const { flagged, reasons } = detectBug(msg.message);
guard.stop();
Flags invisible and combining character floods, mention bombs, oversized native-flow / list / carousel payloads, buttonParamsJson that is broken or huge, excessive newlines, over-deep, over-wide and circular structures, and β when you pass proto β an encoded size over maxBytes.
autoDelete | true | Delete a flagged inbound message. |
deleteMode | 'auto' | 'auto' revokes your own messages and deletes others locally; 'everyone' always revokes. |
guardIncoming | true | Guard messages.upsert. |
guardOutgoing | false | Wrap sock.sendMessage and throw on a crash payload. |
blockOnBug | false | Block the sender of a flagged message. |
selfOnly | false | Only guard your own chats. |
burstThreshold / burstWindowMs | 0 / 4000 | Messages from one sender inside the window before it counts as a burst. 0 disables. |
kickOnBurst | false | Remove the sender from the group on burst (needs admin). |
guardGroupAdds | false | Flag / kick when a burst-flagged inviter adds you to a group. |
metaAiNumbers | false | Guard Meta AI's own numbers instead of exempting them. |
thresholds | {} | Override any of ANCHORGUARD_DEFAULTS. |
[!NOTE]
guardOutgoing replaces sock.sendMessage with a wrapper, which is why it is off by default β turn it on knowingly if your own code also wraps sendMessage. stop() only unwinds its own wrapper, so a wrapper you installed afterwards survives.
π‘ Events
sock.ev.on("connection.update", (update) => {});
sock.ev.on("creds.update", (update) => {});
sock.ev.on("messaging-history.set", (update) => {});
sock.ev.on("messaging-history.status", (update) => {});
sock.ev.on("chats.upsert", (update) => {});
sock.ev.on("chats.update", (update) => {});
sock.ev.on("chats.delete", (update) => {});
sock.ev.on("chats.lock", (update) => {});
sock.ev.on("presence.update", (update) => {});
sock.ev.on("contacts.upsert", (update) => {});
sock.ev.on("contacts.update", (update) => {});
sock.ev.on("messages.delete", (update) => {});
sock.ev.on("messages.update", (update) => {});
sock.ev.on("messages.media-update", (update) => {});
sock.ev.on("messages.upsert", (update) => {});
sock.ev.on("messages.reaction", (update) => {});
sock.ev.on("message-receipt.update", (update) => {});
sock.ev.on("groups.upsert", (update) => {});
sock.ev.on("groups.update", (update) => {});
sock.ev.on("group-participants.update", (update) => {});
sock.ev.on("group.join-request", (update) => {});
sock.ev.on("group.member-tag.update", (update) => {});
sock.ev.on("blocklist.set", (update) => {});
sock.ev.on("blocklist.update", (update) => {});
sock.ev.on("username.update", ({ lid, username, type }) => {});
sock.ev.on("lid-mapping.update", ({ oldLid, newLid, pn }) => {});
sock.ev.on("privacy.update", (settings) => {});
sock.ev.on("call", (update) => {});
sock.ev.on("labels.edit", (update) => {});
sock.ev.on("labels.association", (update) => {});
sock.ev.on("newsletter.reaction", (update) => {});
sock.ev.on("newsletter.view", (update) => {});
sock.ev.on("newsletter.status", (update) => {});
sock.ev.on("newsletter-participants.update", (update) => {});
sock.ev.on("newsletter-settings.update", (update) => {});
sock.ev.on("settings.update", (update) => {});
π° newsletter.status
Fires for channel (newsletter) posts pushed by the server, carrying the post content alongside its engagement counters.
sock.ev.on("newsletter.status", (update) => {
console.log("π° Newsletter post", ":", update);
});
content.type is one of text, media (with an extra mediaType), reaction (with a code), or revoke. It is null when the stanza carries only counter updates.
π call
The call event now carries the full signalling payload.
sock.ev.on("call", ([call]) => {
console.log("π Call", ":", call);
});
callKey | status: 'offer' | Raw SRTP key bytes, Signal-decrypted from the offer |
audioCodecs, audioCodec | status: 'offer' | Offered audio codecs, one entry per sample rate |
videoCodec | status: 'offer' | Offered video codec |
isLightweight | status: 'offer' | Silent / wave-style group ring β no full ringing UX expected |
silenceReason | status: 'offer' | Why the call was silenced |
peerJid | status: 'waiting_room_request' | Who is waiting in a call link's waiting room |
participants | status: 'group_info' | Group call roster, with per-user devices |
muted | status: 'mute' | Mute state of the peer |
enabled | status: 'video_state' | Whether the peer's video is on |
state | status: 'peer_state' | Peer connection state |
latencyMs | status: 'relaylatency' | Relay latency |
status can also be a specific end-call reason instead of a bare terminate: timeout, reject_do_not_disturb, mic_permission_denied, camera_permission_denied, remote_busy, or remote_offline.
Development checks
npm ci
npm run check
npm audit --omit=dev --audit-level=high
npm run check checks JavaScript syntax, imports the package entry point, compiles
a TypeScript consumer, and runs the offline regression suite. Tests do not connect
to WhatsApp. npm publish runs the same checks through prepublishOnly; the CI
workflow also runs them on Node.js 20 and 24.
π¦ Fork Base
@violetix/baileys is a fork of @itsliaaa/baileys by Lia Wynn, which is itself based on Baileys (GitHub). The credits section below is kept in full, as its own notice and the MIT licence both require.
π£ Credits
This fork uses Protocol Buffer definitions maintained by WPP Connect via wa-proto
Full credit is attributed to the original maintainers and contributors of Baileys:
This fork includes additional enhancements and modifications by Lia Wynn
Special thanks to itsreimau for the fix to the updateBlockStatus implementation.
[!CAUTION]
β οΈ Modification, removal, or misrepresentation of these credits is strictly prohibited. Any redistribution or fork must preserve this section in its original form without exception.