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

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

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 architectureWhatsAppClient exposes focused resource modules (messages, chats, contacts, groups, presence, profile).
  • Rich TypeScript interfaces — strongly typed configuration, events, message payloads, and JID utilities.
  • Strongly-typed event buson/once/off/waitFor with 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 IncomingMessage view, 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-api

Quick 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-terminal
import 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); // Buffer

Contacts

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 .gitignore already excludes auth_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 build

Outputs:

  • dist/index.js — ESM entry
  • dist/index.cjs — CommonJS entry
  • dist/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