@binary-black-holes/whatsapp-api
v0.1.0
Published
Type-safe, class-oriented WhatsApp personal-account integration module (QR / pairing-code login) built on the WhatsApp Web protocol — no WhatsApp Business API required.
Maintainers
Readme
@binary-black-holes/whatsapp-api
Type-safe, class-oriented WhatsApp integration module for personal accounts, linked over the WhatsApp Web protocol via QR code or pairing code — no WhatsApp Business API (WABA) and no Meta developer app required.
Built with Vite library mode, rich TypeScript interfaces, declarative JSDoc, and semantic versioning. Powered by Baileys, a pure-WebSocket WhatsApp Web client (no browser/Puppeteer).
Features
- Personal account, no WABA — link a normal WhatsApp account by scanning a QR code or entering a pairing code, exactly like WhatsApp Web / Linked Devices.
- Class-oriented architecture —
WhatsAppClientexposes focused resource modules (messages,chats,contacts,groups,presence,profile). - Rich TypeScript interfaces — strongly typed configuration, events, message payloads, and JID utilities.
- Strongly-typed event bus —
on/once/off/waitForwith a compile-time-checked event map. - Resilient connection — automatic reconnection with exponential backoff and clear lifecycle events.
- Pluggable sessions — filesystem and in-memory auth-state providers, or supply your own (database, Redis, …).
- Dual package exports — ESM (
import) and CommonJS (require) with bundled declaration files. - Normalized messages — an ergonomic
IncomingMessageview, with the raw protocol message always available.
Requirements
- Node.js 18 or newer
- A personal WhatsApp account on a phone (to link the device)
[!IMPORTANT] This SDK automates a personal WhatsApp account through the unofficial WhatsApp Web protocol. It is not the WhatsApp Business Platform / Cloud API. Automating personal accounts can violate WhatsApp's Terms of Service and may lead to your number being banned. Use responsibly, at your own risk, and never for spam.
Installation
npm install @binary-black-holes/whatsapp-apiQuick Start: QR code login
import {
WhatsAppClient,
createMultiFileAuthState,
} from "@binary-black-holes/whatsapp-api";
// Persist the session so you only scan once.
const auth = await createMultiFileAuthState("./auth");
const client = new WhatsAppClient({ auth });
client.on("qr", (qr) => {
// `qr` is the raw QR string. Render it however you like — see "Rendering the QR code" below.
console.log("Scan this QR code with WhatsApp → Linked Devices:", qr);
});
client.on("ready", (me) => console.log("Connected as", me.id));
client.on("message", async (msg) => {
if (msg.fromMe) return;
console.log(`${msg.author}: ${msg.text}`);
if (msg.text?.toLowerCase() === "ping") {
await client.messages.reply(msg, "pong 🏓");
}
});
await client.connect();
await client.messages.sendText("15551234567", "Hello from the SDK!");Quick Start: Pairing-code login
Prefer entering an 8-character code on your phone instead of scanning a QR? Use loginMethod: "pairing-code" and supply your account's phone number (digits only, international format).
import {
WhatsAppClient,
createMultiFileAuthState,
} from "@binary-black-holes/whatsapp-api";
const client = new WhatsAppClient({
auth: await createMultiFileAuthState("./auth"),
loginMethod: "pairing-code",
phoneNumber: "15551234567",
});
client.on("pairing-code", (code) => {
// On your phone: WhatsApp → Linked Devices → Link a device → Link with phone number
console.log("Enter this pairing code on your phone:", code);
});
client.on("ready", () => console.log("Linked!"));
await client.connect();Rendering the QR code
The SDK emits the raw QR string and stays dependency-free about how you display it. A common choice for terminals:
npm install qrcode-terminalimport qrcode from "qrcode-terminal";
client.on("qr", (qr) => qrcode.generate(qr, { small: true }));For web apps, pass the string to any QR component (e.g. qrcode.toDataURL(qr)).
Architecture
WhatsAppClient
├── messages → send/reply/react/edit/delete/forward, read receipts, media download
├── chats → read state, mute, archive, pin, delete, disappearing messages
├── contacts → registration check, profile pictures, status, block/unblock
├── groups → create, membership, admin roles, metadata, invite links
├── presence → online/offline, typing, recording, presence subscription
└── profile → own display name, status, and profile picture
Connection → owns the Baileys socket, login, reconnection, event bridging
EventBus → strongly-typed on/once/off/waitFor over WhatsAppEventMap
AuthStateProvider → pluggable session storage (multi-file, in-memory, custom)Each resource extends BaseResource and borrows the single shared connection on demand, so a transparent reconnect re-points every resource at the new socket automatically.
Events
Subscribe with the strongly-typed on (it returns an unsubscribe function):
const off = client.on("message", (msg) => console.log(msg.text));
off(); // stop listening| Event | Payload | Description |
| --------------------------- | -------------------------- | --------------------------------------------------- |
| qr | string | A QR string is ready to be rendered/scanned. |
| pairing-code | string | A pairing code was issued for phone-number linking. |
| connecting | — | The transport began connecting. |
| ready | ConnectedAccount | Connection open and authenticated. |
| disconnected | DisconnectedEvent | Connection closed (inspect reconnecting). |
| logged-out | — | Session invalidated; re-authentication required. |
| connection.update | ConnectionStatus | High-level status changed. |
| message | IncomingMessage | New inbound (or self-echo) message. |
| message.update | WAMessageUpdate[] | Delivery/read/edit/revoke updates. |
| message.reaction | ReactionEvent | A reaction was added or removed. |
| contacts.update | Partial<Contact>[] | Contacts added/changed. |
| chats.update | Partial<Chat>[] | Chats added/changed. |
| groups.update | Partial<GroupMetadata>[] | Group metadata changed. |
| group.participants.update | GroupParticipantsEvent | Members joined/left/role-changed. |
| presence.update | PresenceEvent | A contact's presence changed. |
| error | Error | An SDK or listener error occurred. |
API overview
Messages
await client.messages.sendText("15551234567", "Hello", {
mentions: ["15559998888"],
});
await client.messages.sendImage(
"15551234567",
{ url: "./photo.jpg" },
{
caption: "Sunset 🌅",
},
);
await client.messages.sendVideo("15551234567", videoBuffer, {
caption: "Clip",
});
await client.messages.sendAudio("15551234567", voiceBuffer, {
voiceNote: true,
});
await client.messages.sendDocument("15551234567", pdfBuffer, {
fileName: "invoice.pdf",
mimetype: "application/pdf",
});
await client.messages.sendLocation("15551234567", {
latitude: 37.422,
longitude: -122.084,
name: "Googleplex",
});
// React, reply, edit, delete, forward
await client.messages.react(message, "🔥");
await client.messages.reply(message, "Got it!");
const sent = await client.messages.sendText("15551234567", "tpyo");
await client.messages.edit(sent!, "typo, fixed");
await client.messages.delete(message, /* forEveryone */ true);
await client.messages.forward("15553334444", message);
// Read receipts and media
await client.messages.markAsRead(message);
const bytes = await client.messages.downloadMedia(message); // BufferContacts
const [result] = await client.contacts.isRegistered("15551234567");
if (result.exists) {
await client.messages.sendText(result.jid!, "You're on WhatsApp!");
}
await client.contacts.getProfilePictureUrl("15551234567");
await client.contacts.getStatus("15551234567");
await client.contacts.block("15551234567");
await client.contacts.unblock("15551234567");
await client.contacts.listBlocked();Groups
const group = await client.groups.create("Project X", [
"15551112222",
"15553334444",
]);
await client.groups.setSubject(group.id, "Project X — Q3");
await client.groups.setDescription(group.id, "Where the magic happens");
await client.groups.addParticipants(group.id, ["15555556666"]);
await client.groups.promote(group.id, ["15551112222"]);
await client.groups.setMessagesAdminsOnly(group.id, true);
const link = await client.groups.getInviteLink(group.id);
await client.groups.join("https://chat.whatsapp.com/XXXXXXXXXXXX");
await client.groups.leave(group.id);
const all = await client.groups.listJoined();Presence
await client.presence.subscribe("15551234567");
await client.presence.startTyping("[email protected]");
await client.presence.stopTyping("[email protected]");
await client.presence.setOnline();Chats
await client.chats.markRead("15551234567");
await client.chats.archive("15551234567", true);
await client.chats.pin("15551234567", true);
await client.chats.mute("15551234567", Date.now() + 8 * 60 * 60 * 1000);
await client.chats.setDisappearing("15551234567", 7 * 24 * 60 * 60);Profile
await client.profile.setName("Ada Lovelace");
await client.profile.setStatus("Computing ✨");
await client.profile.setPicture({ url: "./avatar.png" });Sessions & authentication state
A session is the cryptographic material that keeps your device linked. Persist it so users only authenticate once.
import {
createMultiFileAuthState, // filesystem (recommended)
createInMemoryAuthState, // ephemeral (tests/scripts)
} from "@binary-black-holes/whatsapp-api";
const auth = await createMultiFileAuthState("./auth");[!WARNING] Session files are equivalent to a logged-in device. Never commit them or expose them publicly. The package's
.gitignorealready excludesauth_info/-style folders — keep yours ignored too.
Custom session storage
Implement AuthStateProvider to store sessions anywhere (Postgres, Redis, S3, …):
import type { AuthStateProvider } from "@binary-black-holes/whatsapp-api";
const provider: AuthStateProvider = {
state, // an AuthenticationState you load/build
saveCreds: async () => {
/* persist state.creds */
},
close: async () => {
/* optional teardown */
},
};JIDs (addresses)
WhatsApp addresses chats by JID (e.g. [email protected], 120363…@g.us). The send helpers accept either a JID or a bare phone number — numbers are normalized automatically.
import {
normalizeToJid,
classifyJid,
isGroupJid,
phoneNumberFromJid,
} from "@binary-black-holes/whatsapp-api";
normalizeToJid("+1 (555) 123-4567"); // "[email protected]"
classifyJid("[email protected]"); // "group"
phoneNumberFromJid("[email protected]"); // "15551234567"Error handling
All failures extend WhatsAppError:
| Class | Typical cause |
| --------------------- | ----------------------------------------------------------------- |
| NotConnectedError | An operation was attempted before connect() / after disconnect. |
| AuthenticationError | The session was logged out or revoked. |
| ValidationError | Invalid SDK input before a request. |
| NotFoundError | A target JID is not a registered WhatsApp user. |
| TimeoutError | An operation exceeded its timeout. |
| TransportError | An error surfaced by the WhatsApp Web transport. |
import {
NotConnectedError,
AuthenticationError,
} from "@binary-black-holes/whatsapp-api";
try {
await client.messages.sendText("15551234567", "hi");
} catch (error) {
if (error instanceof NotConnectedError) {
await client.connect();
} else if (error instanceof AuthenticationError) {
// re-scan QR / re-pair
}
}Configuration
const client = new WhatsAppClient({
auth, // AuthStateProvider (defaults to in-memory)
loginMethod: "qr", // or "pairing-code"
phoneNumber: "15551234567", // required for "pairing-code"
browser: ["My App", "Chrome", "1.0.0"], // shown under Linked Devices
markOnlineOnConnect: true,
syncFullHistory: false,
defaultQueryTimeoutMs: 60_000,
reconnect: {
enabled: true,
maxRetries: 10,
baseDelayMs: 2_000,
maxDelayMs: 30_000,
},
logger: console, // any { debug, info, warn, error }
});Lifecycle
await client.connect(); // open + authenticate
client.status; // "idle" | "connecting" | "connected" | "reconnecting" | "logged-out" | "closed"
client.me; // ConnectedAccount | undefined
await client.destroy(); // close without invalidating the session (reusable later)
await client.logout(); // invalidate the session (re-auth required next time)Development
npm install
npm run typecheck
npm test
npm run buildOutputs:
dist/index.js— ESM entrydist/index.cjs— CommonJS entrydist/index.d.ts— bundled type declarations
Semantic versioning
This package follows Semantic Versioning:
- MAJOR — incompatible public API changes
- MINOR — backward-compatible functionality
- PATCH — backward-compatible bug fixes
See CHANGELOG.md for release history.
Notes & limitations
- Built on the unofficial WhatsApp Web protocol via Baileys; WhatsApp may change it at any time.
- Chat-state operations (mute/archive/pin) depend on history sync, which streams in shortly after
ready. - Media helpers may require optional native peers (e.g.
sharp,jimp,link-preview-js) for certain transforms; install them only if you hit a related runtime hint. - This is not affiliated with or endorsed by WhatsApp or Meta.
License
MIT
