@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-gatewayOptional peer dependencies (install only what you need):
# For Redis drivers
npm install ioredis
# For WebSocket event driver
npm install wsQuick 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-eventsWith 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
