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

@besoft-kg/wa-gateway

v0.1.2

Published

Universal WhatsApp Gateway — multi-instance manager with pluggable drivers

Readme

WA Gateway

Universal WhatsApp Gateway — multi-instance manager with pluggable drivers for commands, events, and session storage.

Features

  • Multi-instance — manage multiple WhatsApp connections in a single process
  • Pluggable command drivers — receive commands via HTTP, Redis, or custom driver
  • Pluggable event drivers — emit events via webhook, Redis pub/sub, WebSocket, or custom driver
  • Pluggable session store — store sessions on filesystem or implement your own
  • Programmatic API — use as a library with .on() listeners
  • Auto-reconnect — automatic reconnection with exponential backoff
  • Ban detection — detects account bans and emits events
  • Health monitoring — track instance status in real-time
  • TypeScript — full type safety

Installation

npm install @besoft/wa-gateway

Optional peer dependencies (install only what you need):

# For Redis drivers
npm install ioredis

# For WebSocket event driver
npm install ws

Quick Start

As a library

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({ printQr: true })

gw.on('instance.qr', (data) => {
  console.log('Scan QR code for instance:', data.instanceId)
})

gw.on('instance.connected', (data) => {
  console.log('Connected!', data.instanceId, data.phone)
})

gw.on('message.received', (data) => {
  console.log(`[${data.instanceId}] ${data.from}: ${data.text}`)
})

await gw.start()
await gw.createInstance('my-whatsapp')

As a standalone HTTP service

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({
  commands: [
    { driver: 'http', port: 3100, apiKey: 'my-secret-key' },
  ],
  events: [
    { driver: 'webhook', url: 'https://myapp.com/wa-events' },
  ],
})

await gw.start()
// Gateway is now accepting HTTP commands on port 3100
// Events will be POSTed to https://myapp.com/wa-events

With Redis (for microservice architecture)

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({
  commands: [
    { driver: 'redis', queue: 'wa:commands', host: 'localhost' },
  ],
  events: [
    { driver: 'redis', channel: 'wa:events', host: 'localhost' },
  ],
})

await gw.start()

Full configuration

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({
  // Where to receive commands from
  commands: [
    { driver: 'http', port: 3100, apiKey: 'secret' },
    { driver: 'redis', queue: 'wa:commands', host: 'localhost', port: 6379 },
  ],

  // Where to send events to
  events: [
    { driver: 'webhook', url: 'https://myapp.com/hook', events: ['message.received', 'instance.connected'] },
    { driver: 'redis', channel: 'wa:events', host: 'localhost' },
    { driver: 'websocket', port: 3101 },
  ],

  // Session storage
  store: { driver: 'file', path: './sessions' },

  // Instance defaults
  autoReconnect: true,
  maxReconnectRetries: 5,
  healthCheckInterval: 30000,
  printQr: true,
  logLevel: 'info',
})

await gw.start()

Configuration

GatewayConfig

| Option | Type | Default | Description | |---|---|---|---| | commands | CommandDriverConfig[] | [] | Command driver configurations | | events | EventDriverConfig[] | [] | Event driver configurations | | store | StoreConfig | { driver: 'file', path: './sessions' } | Session store configuration | | autoReconnect | boolean | true | Auto-reconnect on disconnect | | maxReconnectRetries | number | 5 | Max reconnect attempts before giving up | | healthCheckInterval | number | 30000 | Health check interval in ms | | printQr | boolean | true | Print QR code in terminal | | logLevel | string | 'info' | Log level: silent, info, debug, warn, error |

Programmatic API

Gateway methods

const gw = createGateway(config)

// Lifecycle
await gw.start()
await gw.stop()

// Instance management
await gw.createInstance('instance-id')
await gw.deleteInstance('instance-id')
await gw.restartInstance('instance-id')
const instances = gw.listInstances()

// Instance info
const status = gw.getStatus('instance-id')
const qr = gw.getQr('instance-id')

// Messaging
await gw.sendMessage('instance-id', '996555123456', 'text', 'Hello!')
await gw.sendMessage('instance-id', '996555123456', 'image', 'https://example.com/photo.jpg', {
  caption: 'Check this out',
})
await gw.sendMessage('instance-id', '996555123456', 'document', '/path/to/file.pdf', {
  filename: 'report.pdf',
  caption: 'Monthly report',
})

// Number validation
const result = await gw.checkNumber('instance-id', '996555123456')
// { exists: true, jid: '[email protected]' }

// Event listeners
gw.on('message.received', (data) => { ... })
gw.off('message.received', handler)

Send message types

// Text
await gw.sendMessage(instanceId, to, 'text', 'Hello world')

// Image (URL or file path)
await gw.sendMessage(instanceId, to, 'image', 'https://example.com/photo.jpg', {
  caption: 'Optional caption',
})

// Video
await gw.sendMessage(instanceId, to, 'video', 'https://example.com/video.mp4', {
  caption: 'Optional caption',
})

// Audio
await gw.sendMessage(instanceId, to, 'audio', 'https://example.com/audio.mp3')

// Document
await gw.sendMessage(instanceId, to, 'document', '/path/to/file.pdf', {
  filename: 'report.pdf',
  caption: 'Optional caption',
})

Instance status

const info = gw.getStatus('instance-id')
// {
//   id: 'instance-id',
//   status: 'connected',     // 'starting' | 'qr_pending' | 'connected' | 'disconnected' | 'banned' | 'destroyed'
//   phone: '996555123456',
//   createdAt: Date,
//   lastConnectedAt: Date
// }

Command Drivers

Command drivers define how the gateway receives instructions (create instance, send message, etc.).

HTTP Command Driver

Exposes a REST API for managing instances and sending messages.

{
  driver: 'http',
  port: 3100,         // Required
  apiKey: 'secret',   // Optional, enables X-API-Key header auth
  host: '0.0.0.0',   // Optional, default: '0.0.0.0'
}

HTTP API Endpoints

All endpoints require X-API-Key header if apiKey is configured.

Instance management:

# Create instance
curl -X POST http://localhost:3100/instances \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret" \
  -d '{"id": "my-instance"}'

# List all instances
curl http://localhost:3100/instances \
  -H "X-API-Key: secret"

# Get instance status
curl http://localhost:3100/instances/my-instance/status \
  -H "X-API-Key: secret"

# Get QR code
curl http://localhost:3100/instances/my-instance/qr \
  -H "X-API-Key: secret"

# Restart instance
curl -X POST http://localhost:3100/instances/my-instance/restart \
  -H "X-API-Key: secret"

# Delete instance
curl -X DELETE http://localhost:3100/instances/my-instance \
  -H "X-API-Key: secret"

Messaging:

# Send text message
curl -X POST http://localhost:3100/instances/my-instance/send \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret" \
  -d '{"to": "996555123456", "type": "text", "content": "Hello!"}'

# Send image
curl -X POST http://localhost:3100/instances/my-instance/send \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret" \
  -d '{"to": "996555123456", "type": "image", "content": "https://example.com/photo.jpg", "caption": "Look!"}'

# Check if number is on WhatsApp
curl "http://localhost:3100/instances/my-instance/check-number?phone=996555123456" \
  -H "X-API-Key: secret"

Response format:

{
  "success": true,
  "data": { ... }
}

{
  "success": false,
  "error": "Error message"
}

Redis Command Driver

Receives commands from a Redis pub/sub channel. Useful for microservice architectures.

{
  driver: 'redis',
  queue: 'wa:commands',          // Optional, default: 'wa:commands'
  responseChannel: 'wa:responses', // Optional, default: 'wa:responses'
  host: 'localhost',             // Optional, default: 'localhost'
  port: 6379,                    // Optional, default: 6379
  password: '',                  // Optional
}

Redis command format

Publish a JSON message to the wa:commands channel:

{
  "id": "unique-request-id",
  "command": "send_message",
  "params": {
    "instanceId": "my-instance",
    "to": "996555123456",
    "type": "text",
    "content": "Hello!"
  }
}

Response is published to the wa:responses channel:

{
  "id": "unique-request-id",
  "success": true,
  "data": {
    "messageId": "ABCD1234"
  }
}

Available commands

| Command | Params | |---|---| | create_instance | { id: string } | | delete_instance | { id: string } | | restart_instance | { id: string } | | list_instances | {} | | get_status | { instanceId: string } | | get_qr | { instanceId: string } | | send_message | { instanceId, to, type, content, caption?, filename? } | | check_number | { instanceId, phone } |

Custom Command Driver

Implement the CommandDriver interface:

import type { CommandDriver, CommandHandler } from '@besoft/wa-gateway'

class MyCommandDriver implements CommandDriver {
  name = 'my-driver'

  async init(handler: CommandHandler): Promise<void> {
    // Set up your command source (e.g., gRPC, MQTT, etc.)
    // Call handler(command, params) when a command is received

    // Example:
    myMqttClient.on('message', async (topic, msg) => {
      const { command, params } = JSON.parse(msg)
      const result = await handler(command, params)
      // Send result back via your transport
    })
  }

  async destroy(): Promise<void> {
    // Clean up resources
  }
}

// Usage
const gw = createGateway({
  commands: [
    { driver: 'custom', handler: new MyCommandDriver() },
  ],
})

Event Drivers

Event drivers define where the gateway sends events (new messages, status changes, etc.).

Webhook Event Driver

Sends events as HTTP POST requests with automatic retry.

{
  driver: 'webhook',
  url: 'https://myapp.com/wa-events',  // Required
  events: ['message.received', 'instance.connected'],  // Optional, default: ['*'] (all events)
  headers: { 'Authorization': 'Bearer token' },  // Optional, extra headers
  retries: 3,        // Optional, default: 3
  retryDelay: 1000,  // Optional, default: 1000ms (multiplied by attempt number)
  timeout: 10000,    // Optional, default: 10000ms
}

Webhook payload format

{
  "event": "message.received",
  "instanceId": "my-instance",
  "payload": {
    "messageId": "ABCD1234",
    "from": "[email protected]",
    "fromMe": false,
    "text": "Hello!",
    "type": "text",
    "timestamp": 1234567890
  },
  "timestamp": "2025-01-15T10:30:00.000Z"
}

Redis Event Driver

Publishes events to a Redis pub/sub channel.

{
  driver: 'redis',
  channel: 'wa:events',   // Optional, default: 'wa:events'
  host: 'localhost',       // Optional, default: 'localhost'
  port: 6379,              // Optional, default: 6379
  password: '',            // Optional
  events: ['*'],           // Optional, default: ['*']
}

Subscribing to Redis events

import Redis from 'ioredis'

const sub = new Redis()
sub.subscribe('wa:events')

sub.on('message', (channel, message) => {
  const { event, instanceId, payload, timestamp } = JSON.parse(message)
  console.log(`[${instanceId}] ${event}:`, payload)
})

WebSocket Event Driver

Starts a WebSocket server and broadcasts events to all connected clients.

{
  driver: 'websocket',
  port: 3101,        // Required
  events: ['*'],     // Optional, default: ['*']
}

Connecting to WebSocket events

const ws = new WebSocket('ws://localhost:3101')

ws.onmessage = (event) => {
  const { event: eventName, instanceId, payload } = JSON.parse(event.data)
  console.log(`[${instanceId}] ${eventName}:`, payload)
}

Custom Event Driver

Implement the EventDriver interface:

import type { EventDriver } from '@besoft/wa-gateway'

class TelegramEventDriver implements EventDriver {
  name = 'telegram'

  async init(): Promise<void> {
    // Set up Telegram bot
  }

  async emit(event: string, instanceId: string, payload: any): Promise<void> {
    // Send event to Telegram chat
    await telegramBot.sendMessage(chatId, `[${instanceId}] ${event}: ${JSON.stringify(payload)}`)
  }

  async destroy(): Promise<void> {
    // Clean up
  }
}

// Usage
const gw = createGateway({
  events: [
    { driver: 'custom', handler: new TelegramEventDriver() },
  ],
})

Event filtering

Each event driver can filter which events it receives:

events: [
  // This driver only gets message events
  { driver: 'webhook', url: 'https://messages.myapp.com/hook', events: ['message.received', 'message.sent'] },

  // This driver gets all instance status events
  { driver: 'webhook', url: 'https://monitoring.myapp.com/hook', events: ['instance.connected', 'instance.disconnected', 'instance.banned'] },

  // This driver gets everything
  { driver: 'redis', channel: 'wa:all-events', events: ['*'] },
]

Available events

| Event | Payload | Description | |---|---|---| | instance.qr | { instanceId, qr } | QR code generated, scan to authenticate | | instance.connected | { instanceId, phone } | Instance connected to WhatsApp | | instance.disconnected | { instanceId, reason, statusCode? } | Instance disconnected | | instance.banned | { instanceId, reason } | Account banned by WhatsApp | | instance.auth_failure | { instanceId, error } | Authentication failed | | message.received | { instanceId, messageId, from, fromMe, text, type, timestamp, raw } | New message received | | message.sent | { instanceId, messageId, to, type } | Message sent successfully | | message.failed | { instanceId, to, type, error } | Message sending failed | | message.updated | { instanceId, messageId, remoteJid, update } | Message status updated (delivered, read) |

Session Store

File Store (default)

Stores Baileys auth sessions on the filesystem.

{
  store: { driver: 'file', path: './sessions' }
}

Directory structure:

sessions/
├── my-instance-1/
│   ├── creds.json
│   ├── app-state-sync-key-*.json
│   └── ...
├── my-instance-2/
│   └── ...

Custom Store

Implement the SessionStore interface:

import type { SessionStore } from '@besoft/wa-gateway'

class RedisSessionStore implements SessionStore {
  name = 'redis'

  async init(): Promise<void> { ... }
  async getAuthState(instanceId: string): Promise<any> { ... }
  async saveAuthState(instanceId: string, state: any): Promise<void> { ... }
  async deleteSession(instanceId: string): Promise<void> { ... }
  async listSessions(): Promise<string[]> { ... }
  async destroy(): Promise<void> { ... }
}

const gw = createGateway({
  store: { driver: 'custom', handler: new RedisSessionStore() },
})

Examples

Bot that auto-replies

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({ printQr: true })

gw.on('instance.connected', (data) => {
  console.log(`Connected: ${data.phone}`)
})

gw.on('message.received', async (data) => {
  if (data.fromMe) return

  const text = data.text.toLowerCase()

  if (text === 'ping') {
    await gw.sendMessage(data.instanceId, data.from, 'text', 'pong!')
  }

  if (text === 'help') {
    await gw.sendMessage(data.instanceId, data.from, 'text',
      'Available commands:\n- ping\n- help\n- status'
    )
  }
})

await gw.start()
await gw.createInstance('bot')

OTP service

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({
  commands: [{ driver: 'http', port: 3100, apiKey: 'otp-service-key' }],
})

await gw.start()
await gw.createInstance('otp-sender')

// Now your backend can call:
// POST http://localhost:3100/instances/otp-sender/send
// { "to": "996555123456", "type": "text", "content": "Your code: 1234" }

Multi-instance with monitoring

import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({
  commands: [
    { driver: 'http', port: 3100, apiKey: 'admin-key' },
  ],
  events: [
    { driver: 'webhook', url: 'https://myapp.com/wa-events', events: ['message.received'] },
    { driver: 'webhook', url: 'https://monitoring.myapp.com/alerts', events: ['instance.banned', 'instance.disconnected'] },
    { driver: 'websocket', port: 3101 },
  ],
})

gw.on('instance.banned', (data) => {
  console.error(`ALERT: Instance ${data.instanceId} was banned!`)
})

await gw.start()

// Create multiple instances
await gw.createInstance('company-a')
await gw.createInstance('company-b')
await gw.createInstance('company-c')

console.log('Instances:', gw.listInstances())

Using with Redis (microservice)

// wa-gateway-service.ts
import { createGateway } from '@besoft/wa-gateway'

const gw = createGateway({
  commands: [
    { driver: 'redis', queue: 'wa:commands', responseChannel: 'wa:responses' },
  ],
  events: [
    { driver: 'redis', channel: 'wa:events' },
  ],
})

await gw.start()
# your-flask-api.py
import redis
import json
import uuid

r = redis.Redis()

def send_whatsapp_message(instance_id, to, text):
    request_id = str(uuid.uuid4())
    r.publish('wa:commands', json.dumps({
        'id': request_id,
        'command': 'send_message',
        'params': {
            'instanceId': instance_id,
            'to': to,
            'type': 'text',
            'content': text,
        }
    }))
    return request_id

# Listen for events
pubsub = r.pubsub()
pubsub.subscribe('wa:events')

for message in pubsub.listen():
    if message['type'] == 'message':
        event = json.loads(message['data'])
        print(f"[{event['instanceId']}] {event['event']}: {event['payload']}")

Instance Lifecycle

createInstance()
       │
       ▼
   [starting]
       │
       ▼
   [qr_pending] ──── QR scanned ────► [connected]
                                            │
                                     disconnect/error
                                            │
                                            ▼
                                     [disconnected]
                                            │
                              auto-reconnect (exponential backoff)
                                            │
                                            ▼
                                     [starting] ──► ...

                                     if banned:
                                            │
                                            ▼
                                       [banned]

   deleteInstance() ──────────────► [destroyed]

License

MIT