How to Create and Manage WhatsApp Channels or Newsletters Using OpenWA

OpenWA exposes WhatsApp Channels (newsletters) through a REST API and engine layer that maps HTTP requests to whatsapp-web.js BusinessClient methods for subscribing, unsubscribing, and retrieving channel content.

OpenWA (rmyndharis/OpenWA) is a Node.js framework that wraps WhatsApp Web functionality in a NestJS-based architecture. While you cannot create new WhatsApp Channels through the API—since WhatsApp restricts channel creation to the mobile app—you can fully manage subscriptions and consume content programmatically. This guide covers the engine interface, REST endpoints, and implementation details for integrating WhatsApp Channels into your application.

Architecture Overview

OpenWA organizes channel functionality into three distinct layers. The engine interface defines the contract, the adapter implements the logic using whatsapp-web.js, and the controller exposes HTTP endpoints.

The ChannelModule (located in src/modules/channel/channel.module.ts) registers the controller with the global SessionModule, ensuring channel endpoints are available once a session is initialized.

Engine Interface and Implementation

The IWhatsAppEngine interface declares five core methods for channel management. These are implemented in the WhatsAppWebJsAdapter class, which casts the generic client to BusinessClient to access WhatsApp Business API features.

Interface Definition

According to src/engine/interfaces/whatsapp-engine.interface.ts, the contract includes:

export interface IWhatsAppEngine {
  getSubscribedChannels(): Promise<Channel[]>;
  getChannelById(channelId: string): Promise<Channel | null>;
  subscribeToChannel(inviteCode: string): Promise<Channel>;
  unsubscribeFromChannel(channelId: string): Promise<void>;
  getChannelMessages(channelId: string, limit?: number): Promise<ChannelMessage[]>;
}

Adapter Implementation

In src/engine/adapters/whatsapp-web-js.adapter.ts, the adapter implements getSubscribedChannels by calling the underlying library and normalizing the response:

async getSubscribedChannels(): Promise<Channel[]> {
  this.ensureReady();
  const channels = await (this.client as unknown as BusinessClient).getChannels();
  if (!channels) return [];
  return channels.map((ch: WwjsChannelData) => ({
    id: String(typeof ch.id === 'object' ? ch.id._serialized : ch.id),
    name: String(ch.name || ''),
    description: ch.description ? String(ch.description) : undefined,
    inviteCode: ch.inviteCode ? String(ch.inviteCode) : undefined,
    subscriberCount: ch.subscriberCount ? Number(ch.subscriberCount) : undefined,
    verified: ch.verified ? Boolean(ch.verified) : undefined,
  }));
}

Each method begins with ensureReady() (lines 187–191 in the adapter), which validates that the WhatsApp client has reached the READY state before executing Business API calls.

REST API Endpoints

The ChannelController (defined in src/modules/channel/channel.controller.ts) exposes five endpoints that authenticate via sessionId parameter and proxy requests to the engine layer:

  • GET /sessions/:sessionId/channels – Returns all subscribed channels via getSubscribedChannels()
  • POST /sessions/:sessionId/channels/subscribe – Accepts an inviteCode in the body and calls subscribeToChannel()
  • GET /sessions/:sessionId/channels/:channelId – Fetches specific channel details using getChannelById()
  • GET /sessions/:sessionId/channels/:channelId/messages – Retrieves messages with optional limit query parameter via getChannelMessages()
  • DELETE /sessions/:sessionId/channels/:channelId – Unsubscribes from a channel using unsubscribeFromChannel()

The controller retrieves the engine instance through SessionService.getEngine(sessionId), ensuring that requests are routed to the correct WhatsApp Web client.

Code Examples

Listing and Subscribing via HTTP API

Use standard HTTP requests to manage channel subscriptions. All endpoints return JSON objects matching the Channel or ChannelMessage interfaces defined in the engine layer.


# List all subscribed channels for a specific session

curl -X GET "https://your-host/api/sessions/abc123/channels" \
     -H "Authorization: Bearer <your-token>"

# Subscribe to a channel using an invite code

curl -X POST "https://your-host/api/sessions/abc123/channels/subscribe" \
     -H "Content-Type: application/json" \
     -d '{"inviteCode":"ABC123xyz"}'

# Retrieve the latest 20 messages from a channel

curl -X GET "https://your-host/api/sessions/abc123/channels/12345/messages?limit=20"

# Unsubscribe from a channel

curl -X DELETE "https://your-host/api/sessions/abc123/channels/12345"

Managing Channels with the JavaScript SDK

When using the OpenWA SDK client, channel operations mirror the REST API but handle session management internally:

import { OpenWA } from '@open-wa/sdk';

const client = new OpenWA({ sessionId: 'abc123' });
await client.start();

// Fetch all subscribed channels
const channels = await client.getSubscribedChannels();
console.log(`Found ${channels.length} channels`);

// Subscribe using an invite link code
const newChannel = await client.subscribeToChannel('XYZ789abc');
console.log(`Subscribed to: ${newChannel.name}`);

// Get recent messages (defaults to 50 if limit omitted)
const messages = await client.getChannelMessages(newChannel.id, 10);
messages.forEach(m => console.log(`[${m.timestamp}] ${m.body}`));

// Unsubscribe when finished
await client.unsubscribeFromChannel(newChannel.id);

The SDK forwards these calls to the same engine methods used by the REST controller, ensuring consistent behavior across interfaces.

Integrating Channel Logic into NestJS Services

For custom business logic, inject the SessionService directly and interact with the engine layer:

import { Injectable } from '@nestjs/common';
import { SessionService } from '../session/session.service';

@Injectable()
export class NewsletterService {
  constructor(private readonly sessionService: SessionService) {}

  async getAllChannels(sessionId: string) {
    const engine = this.sessionService.getEngine(sessionId);
    return engine.getSubscribedChannels();
  }

  async joinChannel(sessionId: string, inviteCode: string) {
    const engine = this.sessionService.getEngine(sessionId);
    return engine.subscribeToChannel(inviteCode);
  }

  async leaveChannel(sessionId: string, channelId: string) {
    const engine = this.sessionService.getEngine(sessionId);
    await engine.unsubscribeFromChannel(channelId);
  }
}

Summary

  • OpenWA maps WhatsApp Channels to a structured API through the IWhatsAppEngine interface and WhatsAppWebJsAdapter implementation.
  • File locations: Interface definitions reside in src/engine/interfaces/whatsapp-engine.interface.ts (lines 75–81), while the concrete logic is in src/engine/adapters/whatsapp-web-js.adapter.ts (lines 96–215).
  • REST endpoints are exposed under /sessions/:sessionId/channels via ChannelController in src/modules/channel/channel.controller.ts.
  • Operations supported: Listing subscribed channels, subscribing via invite code, fetching channel metadata, retrieving message history, and unsubscribing.
  • Requirement: The WhatsApp client must be in READY state, enforced by the ensureReady() method in the adapter.

Frequently Asked Questions

How do I subscribe to a WhatsApp Channel using OpenWA?

Send a POST request to /sessions/:sessionId/channels/subscribe with a JSON body containing the inviteCode, or call engine.subscribeToChannel(inviteCode) directly. The adapter validates the client state, forwards the request to whatsapp-web.js, and returns a normalized Channel object containing the channel ID, name, and subscriber count.

Can I create a new WhatsApp Channel using OpenWA?

No, OpenWA only supports subscribing to existing channels and retrieving their content. WhatsApp restricts channel creation to the mobile application interface, so the subscribeToChannel method requires an existing inviteCode generated from the WhatsApp mobile client.

What data is returned when retrieving channel messages?

The getChannelMessages method returns an array of ChannelMessage objects, each containing properties such as id, body, timestamp, and author. This data is extracted from the BusinessClient response in whatsapp-web-js.adapter.ts (lines 197–215) and formatted to provide a stable API contract independent of the underlying library's raw output.

How does OpenWA handle multiple WhatsApp sessions with channels?

Each session maintains its own engine instance via the SessionService. When you call endpoints under /sessions/:sessionId/channels, the controller looks up the specific engine for that session ID. This isolation ensures that channel subscriptions and message history remain separate across different WhatsApp accounts managed by the same OpenWA instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →