Skip to main content

WhatsApp channel — developer reference

This page covers what plugin authors need to know when writing plugins that interact with the whatsapp channel: channel identifier, hook filtering patterns, available message actions, and channel-specific behavior to account for in plugin code. For building a new channel adapter from scratch, see Channel adapters. For the admin configuration reference, see Admin: WhatsApp.

Channel identifier

The WhatsApp channel registers under the id "whatsapp". Use this when filtering hooks by channel:
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

export default function register(api: OpenClawPluginApi): void {
  api.on("message_received", async (event, ctx) => {
    if (ctx.channelId !== "whatsapp") return;

    // WhatsApp-specific logic here
    api.logger.info("WhatsApp message received", {
      from: ctx.senderId,
      body: event.body,
    });
  });
}
ctx.channelId is always "whatsapp" for messages, hooks, and tool calls that originate from the WhatsApp channel.

Available hooks (WhatsApp-relevant)

All standard plugin hooks fire for WhatsApp events. The most commonly used for channel-specific behavior:
HookFires whenNotes
message_receivedInbound WhatsApp message acceptedAfter allowlist/policy checks pass
before_tool_callAgent is about to call a toolctx.channelId === "whatsapp" for WhatsApp-originated calls
after_tool_callTool call returnedIncludes result
message_sendingWednesdayAI is about to send a WhatsApp replyCan modify content or cancel
message_sentReply delivered to WhatsApp
session_startNew WhatsApp session created
before_agent_startAgent loop begins for a WhatsApp message

Filtering to a specific account

WhatsApp supports multiple linked accounts. Filter by account using ctx.accountId:
api.on("message_received", async (event, ctx) => {
  if (ctx.channelId !== "whatsapp") return;
  if (ctx.accountId !== "work") return; // only handle the "work" account

  // ...
});

Message actions

WhatsApp supports the following message actions from agent tools or automation:
ActionDescription
sendSend a text message or media
reactSend a reaction emoji to a message
Gating: channels.whatsapp.actions.reactions and channels.whatsapp.actions.polls must be true for those actions to be available.

Using message actions in a plugin tool

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "openclaw/plugin-sdk";

export default function register(api: OpenClawPluginApi): void {
  api.registerTool({
    name: "whatsapp_react",
    description: "React to a WhatsApp message with an emoji",
    parameters: Type.Object({
      messageId: Type.String({ description: "WhatsApp message stanza ID" }),
      emoji: Type.String({ description: "Reaction emoji, e.g. '👍'" }),
    }),
    async execute(_id, params) {
      // Reactions are handled by the channel adapter via message actions.
      // Use api.runtime message action helpers when exposed, or return
      // a structured response the agent can use to drive the action.
      return {
        content: [{ type: "text", text: `Reaction queued: ${params.emoji}` }],
      };
    },
  });
}
Direct message action dispatch from plugin tools is handled through the agent’s response pipeline. Return structured action payloads from your tool and let the channel adapter handle delivery.

Session keys

WhatsApp session keys follow these patterns:
Chat typeSession key pattern
DM (default dmScope)agent:<agentId>:main
DM (per-peer)agent:<agentId>:whatsapp:dm:<peerId>
Groupagent:<agentId>:whatsapp:group:<jid>
Use ctx.sessionKey in hook handlers — do not construct session keys manually.

Channel-specific behaviors to handle in plugins

Baileys dependency is lazy-loaded. The whatsapp bundled extension loads @whiskeysockets/baileys lazily at channel-activation time, not at gateway startup. Plugins that interact with the WhatsApp channel must not assume the Baileys library is available as a direct import — use only the openclaw/plugin-sdk surface. Self-chat. When the linked number is in allowFrom, self-chat safeguards are active: read receipts are skipped and the agent does not trigger on its own outbound messages. Hooks on message_received will not fire for self-sent messages. Socket lock. Only one Baileys socket can own an account’s credentials directory at a time (enforced by a .socket-owner.lock file). If a plugin attempts gateway-level WhatsApp operations, it must not open a second Baileys connection against the same credentials directory. Media placeholders. Inbound media-only messages arrive normalized: <media:image>, <media:video>, <media:audio>, <media:document>, <media:sticker>. The raw Baileys message payload is not passed to hooks. Status and broadcast chats are ignored. Messages from @status and @broadcast JIDs are dropped before routing. Hooks will not fire for these.

Async safety

Plugin tools and hook handlers run in the same Node.js event loop as the gateway. Do not use synchronous blocking I/O in tool execute() functions or hook handlers.
import * as fs from "node:fs";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

// ❌ Do not block the event loop
export default function register(api: OpenClawPluginApi): void {
  const data = fs.readFileSync("./config.json", "utf8");
  // ...
}

// ✅ Use async I/O
export default async function register(api: OpenClawPluginApi): Promise<void> {
  const data = await fs.promises.readFile("./config.json", "utf8");
  // ...
}