@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-sdkAuthentication — 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');
}
}