A modern, state-based WhatsApp bot library for Node.js built on top of GREEN-API.
- State-based conversation flow
- Built-in session management
- Flexible message handling
- Navigation support
- Custom storage extensibility
- TypeScript support
- Automatic instance settings configuration
npm install @green-api/whatsapp-chatbot-js-v2
import { WhatsAppBot, State } from '@green-api/whatsapp-chatbot-js-v2';
// Initialize the bot
const bot = new WhatsAppBot({
idInstance: "your-instance-id",
apiTokenInstance: "your-token",
defaultState: "menu"
});
// Define states
const menuState: State = {
name: "menu",
async onEnter(message) {
await bot.sendText(
message.chatId,
"Welcome! Choose an option:\n1. Help\n2. About"
);
},
async onMessage(message) {
if (message.text === "1") {
return "help";
}
return null; // Continue to global handlers
}
};
// Add states and start the bot
bot.addState(menuState);
bot.start();
Complete configuration options for the WhatsAppBot:
interface BotConfig<T = any> {
/** Green API instance ID */
idInstance: string;
/** Green API instance token */
apiTokenInstance: string;
/** Initial state name for new sessions. Default: "root" */
defaultState?: string;
/** Session timeout in seconds. Default: 300 (5 minutes) */
sessionTimeout?: number;
/** Function to generate session timeout message based on session data */
getSessionTimeoutMessage?: (session: SessionData<T>) => string;
/** Command(s) to trigger back navigation. Default: "back" */
backCommands?: string | string[];
/** Custom storage adapter for session management. Default: MemoryStorage */
storage?: StorageAdapter<T>;
/** Custom settings for the GREEN-API instance */
settings?: {
webhookUrl?: string;
webhookUrlToken?: string;
outgoingWebhook?: "yes" | "no";
stateWebhook?: "yes" | "no";
incomingWebhook?: "yes" | "no";
outgoingAPIMessageWebhook?: "yes" | "no";
outgoingMessageWebhook?: "yes" | "no";
pollMessageWebhook?: "yes" | "no";
markIncomingMessagesReaded?: "yes" | "no";
};
/** Whether to clear webhook queue on bot startup. Default: false */
clearWebhookQueueOnStart?: boolean;
/** Controls message processing order. When true, global handlers run before state handlers (onMessage).
* When false (default), state handlers run first. */
handlersFirst?: boolean;
}
Main class for creating and managing your bot:
const bot = new WhatsAppBot<CustomSessionData>({
// Required parameters
idInstance: "your-instance-id",
apiTokenInstance: "your-token",
// Optional parameters
defaultState: "menu",
sessionTimeout: 30, // 30 seconds
getSessionTimeoutMessage: (session) => {
// Custom logic to return timeout message
return "Your session has expired. Starting over.";
},
backCommands: ["back", "return", "menu"],
storage: new CustomStorage(),
clearWebhookQueueOnStart: true,
// GREEN-API instance settings
settings: {
webhookUrl: "",
webhookUrlToken: "",
outgoingWebhook: "no",
stateWebhook: "no",
incomingWebhook: "yes",
outgoingAPIMessageWebhook: "no",
outgoingMessageWebhook: "no",
pollMessageWebhook: "yes",
markIncomingMessagesReaded: "yes"
}
});
States define the conversation flow:
interface State<T = any> {
name: string;
onEnter?: (message, stateData?) => Promise<void | string | StateTransition>;
onMessage: (message, stateData?) => Promise<void | string | StateTransition | null>;
onLeave?: (message, stateData?) => Promise<void>;
}
State handler return values:
null
: Continue to global handlersundefined
: Stop processingstring
: Transition to that stateStateTransition
: Transition with data
Global message handlers for specific patterns:
// Exact text match
bot.onText("help", async (message) => {
await bot.sendText(message.chatId, "Help message");
});
// Regular expression
bot.onRegex(/^order:\s*(\d+)$/, async (message) => {
const [, orderId] = message.text.match(/^order:\s*(\d+)$/);
await bot.sendText(message.chatId, `Order ${orderId} info...`);
});
// Message type
bot.onType("image", async (message) => {
await bot.sendText(message.chatId, "Image received!");
});
Built-in memory storage with custom adapter support:
interface StorageAdapter<T = any> {
/** Optional event emitter for session-related events like expiration */
events?: StorageEventEmitter<T>;
/** Retrieves session data for a chat */
get(chatId: string): Promise<SessionData<T> | null>;
/** Stores session data for a chat */
set(chatId: string, data: SessionData<T>): Promise<void>;
/** Optional method
341A
to receive session timeout value for cleanup processes */
setSessionTimeout?(timeoutMs: number): void;
}
Example custom storage:
class CustomStorage implements StorageAdapter {
async get(chatId: string) {
return await db.sessions.findOne({chatId});
}
async set(chatId: string, data: SessionData) {
await db.sessions.upsert({chatId, ...data});
}
}
The bot uses a state-based architecture where each state represents a specific point in the conversation flow. States can have entry points, message handlers, and exit points.
Called when the bot enters a state. Useful for sending initial messages or setting up state data.
const menuState: State = {
name: "menu",
async onEnter(message) {
await bot.sendText(message.chatId, "Welcome to the menu!");
}
};
Return values from onEnter:
void
- Stay in current statestring
- Name of state to transition toStateTransition
- Transition with data:return { state: "next_state", data: { someData: "value" }, skipOnEnter: false // Optional: skip next state's onEnter };
Handles messages received while in this state.
const orderState: State<OrderData> = {
name: "create_order",
async onMessage(message, data = {items: []}) {
if (message.text === "done") {
return "confirm_order";
}
// Add item to order
data.items.push(message.text);
return {
state: "create_order", // Stay in same state
data: data // Update state data
};
}
};
Return values from onMessage:
null
- Continue to global handlers (like back command)undefined
- Stop processingstring
- Name of state to transition toStateTransition
- Transition with data
Called when leaving a state. Use for cleanup or final processing.
const paymentState: State = {
name: "payment",
async onLeave(message, data) {
await savePaymentAttempt(data);
}
};
Sessions maintain conversation state and custom data between messages.
interface SessionData<T = any> {
lastActivity: number; // Timestamp of last activity
currentState?: string; // Current state name
stateData?: T; // Custom state data
navigationPath?: string[]; // History of states
previousState?: string; // Last state (for back navigation)
}
Custom data persists between messages in the same state:
interface OrderData {
orderId: string;
items: string[];
total: number;
}
const bot = new WhatsAppBot<OrderData>();
const orderState: State<OrderData> = {
name: "order",
async onMessage(message, data = {items: [], total: 0}) {
// data is typed as OrderData
data.items.push(message.text);
return {state: "order", data};
}
};
Sessions expire after inactivity (default 300 seconds / 5 minutes). You can customize both the timeout duration and the message sent when a session times out:
const bot = new WhatsAppBot({
// Set timeout to 30 seconds
sessionTimeout: 30,
// Custom timeout message
getSessionTimeoutMessage: (session) => {
// You can access session data to customize the message
const userName = session.stateData?.userName || "User";
return `Hello ${userName}, your session has expired. Starting a new conversation.`;
}
});
The built-in memory storage checks for expired sessions every 10 seconds and automatically removes them. If you implement your own storage adapter, you'll need to implement your own cleanup mechanism.
Storage adapters can optionally emit events when sessions expire. This feature enables the bot to send timeout notifications:
import { EventEmitter } from 'events';
class DatabaseStorage implements StorageAdapter {
public events = new StorageEventEmitter();
constructor(timeoutSeconds: number) {
// Set up cleanup that emits events
setInterval(() => {
// Find expired sessions
const expiredSessions = // ... your logic
for (const session of expiredSessions) {
this.events.emit('sessionExpired', session.chatId, session);
// Delete session
}
}, 10000);
}
}
The events field is optional - if you don't need timeout notifications, you can omit it from your storage implementation. The timeout message is defined in the getSessionTimeoutMessage function.
The bot offers two message processing flows, controlled by the handlersFirst
configuration option:
- Bot receives message
- Current state's
onMessage
handler processes message - Based on return value:
- If
null
: Global handlers process message (onText, onType, onRegex) - If
undefined
: Stop processing - If state name/transition: Enter new state
- If
- Bot receives message
- Global handlers (onText, onRegex, onType) process message
- If a handler returns
true
, continue to state processing - If a handler returns anything else, stop processing
- If a handler returns
- If previous global handler returned
true
, current state'sonMessage
handler processes it
When using the handlers-first approach (handlersFirst: true
), global message handlers support an additional return
value:
- undefined: Message was handled, stop processing (default)
- true: Message was partially handled, continue to state processing
- anything else: Treat as undefined (stop processing)
Messages are processed in a specific priority order:
-
State Handler (First)
- The current state's
onMessage
handler gets the first chance to process every message - Based on its return value:
null
: Continue to global handlersundefined
: Stop processingstring
: Transition to that state and stopStateTransition
: Transition with data and stop
- The current state's
-
Global Handlers (Only if state handler returns null) Messages that aren't fully handled by the state (returned
null
) continue to global handlers in this order:a. Text Handlers
- Exact text matches (case-insensitive)
- First matching handler processes the message and stops further handling
b. Regex Handlers
- Regular expression pattern matches
- First matching pattern processes the message and stops further handling
c. Type Handlers
- Specific type handlers run first
- Generic ("*") handlers run last as fallback
- First matching handler processes the message and stops further handling
This means that if your state's onMessage
handler returns undefined
(the default return value), global handlers will
never run. Make sure to return null
if you want to allow global handlers to process the message.
const bot = new WhatsAppBot({
idInstance: "your-instance-id",
apiTokenInstance: "your-token",
defaultState: "menu",
handlersFirst: true // Process global handlers before state handlers
});
// Handler that fully processes the message (stops further processing)
bot.onText("/help", async (message) => {
await bot.sendText(message.chatId, "Here's the help information...");
// No explicit return - defaults to undefined (stop processing)
});
// Handler that partially processes the message and continues to state handler
bot.onType("location", async (message) => {
await bot.sendText(message.chatId, "I've received your location...");
return true; // Continue to state processing for additional handling
});
Match exact text (case-insensitive):
bot.onText("menu", async (message) => {
await bot.sendText(message.chatId, "Main menu");
});
Match text patterns:
bot.onRegex(/order:\s*(\d+)/, async (message) => {
const [, orderId] = message.text.match(/order:\s*(\d+)/);
await bot.sendText(message.chatId, `Order ${orderId} details...`);
});
Handle specific message types:
// Handle all images
bot.onType("image", async (message) => {
const url = message.media?.url;
await processImage(url);
});
// Handle all messages (fallback)
bot.onType("*", async (message) => {
console.log("Unhandled message:", message);
});
Default storage is in-memory, but you can implement custom storage:
class DatabaseStorage implements StorageAdapter {
// Optional event support for timeout notifications
public events?: StorageEventEmitter;
constructor(timeoutSeconds: number) {
// Optional: Set up events if you want timeout notifications
this.events = new StorageEventEmitter();
this.startCleanup(timeoutSeconds);
}
private async startCleanup(timeoutSeconds: number) {
setInterval(async () => {
// Your cleanup logic
if (this.events) {
// Emit events for expired sessions
this.events.emit('sessionExpired', chatId, session);
}
}, 10000);
}
async get(chatId: string): Promise<SessionData | null> {
return await db.sessions.findOne({chatId});
}
async set(chatId: string, data: SessionData): Promise<void> {
await db.sessions.updateOne(
{chatId},
{$set: data},
{upsert: true}
);
}
}
const bot = new WhatsAppBot({
storage: new DatabaseStorage()
});
const steps: State[] = [
{
name: "step1",
async onMessage(message) {
if (message.text) return "step2";
return undefined;
}
},
{
name: "step2",
async onMessage(message) {
if (message.text) return "step3";
return undefined;
}
}
];
const menuState: State = {
name: "menu",
async onMessage(message) {
switch (message.text) {
case "1":
return "orders";
case "2":
return "settings";
default:
return null;
}
}
};
interface UserData {
name?: string;
age?: number;
}
const nameState: State<UserData> = {
name: "get_name",
async onMessage(message, data = {}) {
return {
state: "get_age",
data: {...data, name: message.text}
};
}
};
const ageState: State<UserData> = {
name: "get_age",
async onMessage(message, data = {}) {
const age = parseInt(message.text);
return {
state: "confirm",
data: {...data, age}
};
}
};
Add type-safe custom data to your states:
interface OrderData {
orderId?: string;
items: string[];
total: number;
}
const bot = new WhatsAppBot<OrderData>({...});
const orderState: State<OrderData> = {
name: "create_order",
async onMessage(message, data = {items: [], total: 0}) {
// Type-safe access to custom data
data.items.push(message.text);
return {
state: "confirm_order",
data: data
};
}
};
Send different types of files:
await bot.sendFile(chatId, {
url: "https://example.com/image.jpg",
type: "image",
caption: "Check this out!"
});
-
State Organization
- Keep states focused and single-purpose
- Use clear naming conventions
- Handle edge cases and errors
-
Error Handling
- Implement global error handlers
- Log errors appropriately
- Provide user-friendly error messages
-
Session Management
- Clean up old sessions regularly
- Implement proper timeout handling
See examples/tickets.ts
for a complete example of a support ticket system demonstrating state
management, file handling, and complex conversation flows.
Check examples/custom-storage/
for an example of implementing a custom storage provider
with a simple bot implementation.
Check out our demo chatbot to see a big scale implementation, based on GREEN-API
MIT