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

@saidksi/whatsapp-sdk

v1.10.0

Published

TypeScript SDK for the WhatsApp API Server — supports both simple session key and full account management key patterns

Readme

@saidksi/whatsapp-sdk

TypeScript SDK for the WhatsApp API server. Works in Node.js and the browser.

Installation

npm install @saidksi/whatsapp-sdk

Authentication — two key types

The API uses two distinct key types:

| Key prefix | Where it comes from | What it can do | |---|---|---| | sk_user_ | Returned when a super_admin creates a user | Manage sessions (create, delete, QR, status, regenerate keys), manage users | | sk_live_ | Returned when a session is created | Send messages, manage webhooks for that session |

Keys are hashed server-side and shown only once — store them securely.

Two clients

import { WhatsAppAdminClient, WhatsAppSessionClient } from '@saidksi/whatsapp-sdk';

// Admin client — uses sk_user_ key
const admin = new WhatsAppAdminClient('sk_user_...', 'http://localhost:3001');

// Session client — uses sk_live_ key, scoped to one session
const session = new WhatsAppSessionClient('sk_live_...', '<sessionId>', 'http://localhost:3001');

Quick start

1. Create a session (two authentication methods)

Option A: QR Code Scanning (recommended)

import QRCode from 'qrcode'; // npm install qrcode

const admin = new WhatsAppAdminClient('sk_user_...', 'http://localhost:3001');

// Create session with QR code — returns a one-time sk_live_ key
const { id: sessionId, apiKey } = await admin.sessions.create({ name: 'My Bot' });

const canvas = document.getElementById('qr-canvas') as HTMLCanvasElement;

// streamQR opens an SSE connection — the server pushes the QR the instant it is generated.
// No polling lag, no expired codes. Resolves automatically once the phone scans the QR.
await admin.sessions.streamQR(sessionId, (event) => {
  if (event.type === 'session.qr') {
    // Render each QR as it arrives — WhatsApp rotates them every ~20 seconds
    QRCode.toCanvas(canvas, event.qr!, { width: 300 });
  }
  if (event.type === 'session.connected') {
    console.log('Connected! Phone:', event.phoneNumber);
  }
});

Option B: Phone Number Pairing (new)

const admin = new WhatsAppAdminClient('sk_user_...', 'http://localhost:3001');

// Create session with phone number pairing — returns pairing code
const { id: sessionId, apiKey, pairingCode } = await admin.sessions.create({
  name: 'My Bot',
  method: 'phone',          // Enable phone pairing
  phoneNumber: '+1234567890' // User's WhatsApp phone number
});

console.log(`Pairing code: ${pairingCode}`);
// User enters this code on their WhatsApp phone: Settings → Linked Devices → Link with Phone Number

// Or stream the pairing code if it's not ready immediately
if (!pairingCode) {
  await admin.sessions.streamPairingCode(sessionId, (event) => {
    if (event.type === 'session.pairing_code') {
      console.log(`Code: ${event.code} (expires in ${event.expiresIn}s)`);
    }
    if (event.type === 'session.connected') {
      console.log('Pairing complete! Phone:', event.phoneNumber);
    }
  });
}

2. Send a message

const session = new WhatsAppSessionClient(apiKey, sessionId, 'http://localhost:3001');

const job = await session.messages.sendText('+1234567890', 'Hello from my bot!');
console.log(job.id, job.status); // job.status === 'queued'

Messages are queued with 15–45 second human-like delays between sends to avoid bot detection.

3. Switch authentication method (or reconnect)

// Switch an existing session to phone pairing
const { pairingCode } = await admin.sessions.connect(sessionId, {
  method: 'phone',
  phoneNumber: '+1234567890'
});

// Or switch back to QR
await admin.sessions.connect(sessionId, { method: 'qr' });

3. List conversations

const session = new WhatsAppSessionClient(apiKey, sessionId, 'http://localhost:3001');

// Returns chat metadata only — no messages embedded
const { chats, pagination } = await session.chats.list({ limit: 30, offset: 0 });

console.log(`${pagination.total} total chats`);
chats.forEach((chat) => {
  console.log(chat.name, chat.unreadCount, chat.lastMessage?.body);
});

// Load next page
if (pagination.hasMore) {
  const page2 = await session.chats.list({ limit: 30, offset: 30 });
}

4. Load messages for a conversation

// First page — latest 30 messages
const { messages, pagination: mp } = await session.chats.getMessages(chat.id, { limit: 30 });

messages.forEach((msg) => {
  const direction = msg.fromMe ? '→' : '←';
  console.log(`${direction} [${msg.type}] ${msg.body}`);
});

// Scroll up — load older messages
if (mp.hasMore) {
  const older = await session.chats.getMessages(chat.id, { limit: 30, before: mp.nextBefore! });
}

// Fetch media for image/video/audio messages
const mediaMsg = messages.find((m) => m.hasMedia);
if (mediaMsg) {
  const { media } = await session.messages.getMedia(mediaMsg.id);
  console.log(media); // { url: 'data:image/jpeg;base64,...', type: 'image' }
}

5. Send a message to a chat

// Use chat.id as the recipient — works for all contact types (@c.us and @lid)
const job = await session.messages.sendText(chat.id, 'Hello from my bot!');
console.log(job.status); // job.status === 'queued'

6. Mark a chat as read

// Clear unread counter for a conversation
await session.chats.markAsRead(chat.id);

7. React to a message

// Add an emoji reaction
await session.messages.react(message.id, '👍');

// Remove the reaction
await session.messages.react(message.id, '');

8. Stream new messages in real-time

const controller = new AbortController();

// streamEvents resolves when the connection closes
session.chats.streamEvents((event) => {
  if (event.type === 'message.received') {
    console.log(`[${event.chatId}] ${event.message?.body}`);
  }
  if (event.type === 'message.sent') {
    console.log(`Sent to ${event.chatId}: ${event.message?.body}`);
  }
}, controller.signal);

// Stop streaming later
controller.abort();

9. Listen for events via webhook

const webhook = await session.webhooks.create({
  label: 'My webhook',
  url: 'https://example.com/webhook',
  events: ['message.received', 'message.sent', 'session.connected'],
  secret: 'my-signing-secret', // optional — used to verify payloads
});

// secret is only shown on creation — store it!
console.log(webhook.secret);

API reference

WhatsAppAdminClient

new WhatsAppAdminClient(apiKey: string, baseUrl?: string, timeout?: number)

| Method | Description | |---|---| | users.create(data) | Create a user (super_admin only) | | users.regenerateKey(userId) | Regenerate a user's sk_user_ key | | sessions.create(data) | Create a session (QR or phone pairing) — returns one-time sk_live_ key | | sessions.connect(sessionId, data) | Switch auth method — reconnect session with QR or phone pairing | | sessions.streamQR(sessionId, onEvent, signal?) | Stream QR via SSE — push-based, no polling lag | | sessions.getQR(sessionId) | Get current QR code (single request) | | sessions.pollQR(sessionId, opts?) | Poll until QR available (legacy — prefer streamQR) | | sessions.streamPairingCode(sessionId, onEvent, signal?) | Stream pairing code via SSE — push-based phone pairing, no polling lag | | sessions.getPairingCode(sessionId) | Get current pairing code (single request) | | sessions.pollPairingCode(sessionId, opts?) | Poll until pairing code available (legacy — prefer streamPairingCode) | | sessions.streamStatus(sessionId, onEvent, signal?) | Stream status via SSE — push connected/disconnected/auth_failure events | | sessions.streamSessions(onEvent, signal?) | Stream all sessions via SSE — multi-session dashboard with live events | | sessions.getStatus(sessionId) | Get connection status and phone number | | sessions.delete(sessionId) | Hard-delete session and all its data | | sessions.regenerateKey(sessionId) | Regenerate session sk_live_ key | | sessions.waitForConnection(sessionId, opts?) | Poll until session is connected (legacy — prefer streamQR or streamPairingCode) | | health() | API health check |

WhatsAppSessionClient

new WhatsAppSessionClient(apiKey: string, sessionId: string, baseUrl?: string, timeout?: number)

| Method | Description | |---|---| | chats.list(params?) | List conversations — paginated chat metadata (limit, offset) | | chats.getMessages(chatId, params?) | Cursor-paginated messages for one conversation (limit, before) | | chats.markAsRead(chatId) | Mark a chat as read, clearing its unread counter | | chats.streamEvents(onEvent, signal?) | Stream new messages in real-time via SSE | | messages.send(data) | Enqueue a message | | messages.sendText(to, body) | Convenience: enqueue a text message | | messages.react(messageId, reaction) | React to a message with an emoji ("" removes the reaction) | | messages.getMedia(messageId) | Fetch media payload for an image/video/audio/document/sticker | | messages.list(params?) | List messages with pagination | | messages.get(messageId) | Get a specific message | | messages.getJobs(params?) | List queue jobs | | messages.streamJobs(onEvent, signal?) | Stream job status via SSE — push-based delivery tracking, no polling | | webhooks.create(data) | Create a webhook subscription | | webhooks.list() | List all webhooks | | webhooks.get(webhookId) | Get a webhook | | webhooks.update(webhookId, data) | Update a webhook | | webhooks.delete(webhookId) | Delete a webhook | | webhooks.test(webhookId) | Send a test ping | | health() | API health check |

Webhook events

| Event | Trigger | |---|---| | session.connected | Session authenticated (QR scanned) | | session.disconnected | Session logged out or lost connection | | session.qr_generated | New QR code generated | | session.auth_failure | Authentication failed | | message.received | Incoming message received | | message.sent | Outbound message sent | | message.delivered | Message delivered to recipient | | message.read | Message read by recipient | | message.failed | Message delivery failed |

Payload verification

Webhook payloads include an X-Webhook-Signature header — HMAC-SHA256 of the raw body using your webhook secret:

import crypto from 'crypto';

function verifySignature(rawBody: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return `sha256=${expected}` === signature;
}

Error handling

import { AuthenticationError, NotFoundError, ValidationError } from '@saidksi/whatsapp-sdk';

try {
  await session.messages.sendText('+1234567890', 'Hello');
} catch (err) {
  if (err instanceof AuthenticationError) {
    console.error('Invalid or expired API key');
  } else if (err instanceof ValidationError) {
    console.error('Bad request:', err.message);
  } else if (err instanceof NotFoundError) {
    console.error('Session not found');
  }
}