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

v2.1.7

Published

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

Downloads

952

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 { sessionId, key } = 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 Baileys generates it.
// 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 { sessionId, key, 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(key, sessionId, 'http://localhost:3001');

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

// Image from URL — caption is optional
const imageJob = await session.messages.sendImage(
  '+1234567890',
  JSON.stringify({ source: 'url', url: 'https://example.com/photo.jpg' }),
  'Optional caption'
);

// Image with no caption
await session.messages.sendImage(
  '+1234567890',
  JSON.stringify({ source: 'url', url: 'https://example.com/photo.jpg' })
);

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(key, 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 ? '→' : '←';
  const sender = msg.fromName ?? msg.from; // display name or identifier
  console.log(`${direction} [${msg.type}] ${sender}: ${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?.fromName ?? event.message?.from}: ${event.message?.body}`);
  }
  if (event.type === 'message.sent') {
    console.log(`Sent to ${event.chatId}: ${event.message?.body}`);
  }
  // Delivery receipts — use to update tick indicators in your UI
  if (event.type === 'message.delivered' || event.type === 'message.read') {
    console.log(`${event.type}: ${event.whatsappMessageId} → ${event.status}`);
  }
}, 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);

10. Configure a dedicated bot webhook

Use the bot endpoint to configure a single webhook with rate limiting and health monitoring:

// Configure a bot webhook for this session
const botConfig = await session.bot.configure({
  webhookUrl: 'https://example.com/bot-webhook',
  eventTypes: ['message.received', 'message.sent'],
  maxConcurrentDeliveries: 5, // concurrent deliveries, 1-100 (default 5)
});

console.log(botConfig.webhookUrl);
console.log(botConfig.eventTypes);
console.log(botConfig.maxConcurrentDeliveries);

// Check bot health and delivery stats
const health = await session.bot.getHealth();
console.log(health.successRate);       // 0-100%
console.log(health.totalDeliveries);   // total attempts
console.log(health.status);            // 'healthy' | 'degraded' | 'failing' | 'no_data'

// Get current bot configuration
const config = await session.bot.getConfig();

// Delete bot configuration (stops webhooks)
await session.bot.delete();

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 and delivery receipts in real-time via SSE | | messages.send(data) | Enqueue a message (body optional for media types) | | messages.sendText(to, body) | Convenience: enqueue a text message | | messages.sendImage(to, mediaPayload, caption?) | Convenience: enqueue an image message — caption is optional | | 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.getJob(jobId) | Get a single queue job by ID | | 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 | | bot.configure(data) | Configure bot webhook (webhook URL, event types, rate limit) | | bot.getConfig() | Get current bot configuration | | bot.getHealth() | Get bot health metrics (success rate, delivery stats) | | bot.delete() | Remove bot configuration | | health() | API health check |

Webhook events

| Event | Trigger | |---|---| | session.connected | Session authenticated (QR scanned or code entered) | | session.disconnected | Session logged out or lost connection | | session.qr_generated | New QR code generated | | session.pairing_code_generated | Pairing code generated for phone-based login | | session.auth_failure | Authentication failed (e.g. bad session, replaced, forbidden) | | 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');
  }
}