Install
openclaw onboard and openclaw channels add --channel whatsapp prompt to install the plugin the first time you select it; openclaw channels login --channel whatsapp offers the same install flow if the plugin is missing. Dev checkouts use the local plugin path; stable/beta installs @openclaw/whatsapp from ClawHub first, falling back to npm. The WhatsApp runtime ships outside the core OpenClaw npm package, so its runtime dependencies stay with the external plugin. Manual install:
@openclaw/whatsapp) only for the registry fallback; pin an exact version only for a reproducible install.
Pairing
Channel troubleshooting
Gateway configuration
Quick setup
Link WhatsApp (QR)
Deployment patterns
Dedicated number (recommended)
Dedicated number (recommended)
- separate WhatsApp identity for OpenClaw
- clearer DM allowlists and routing boundaries
- lower chance of self-chat confusion
Personal-number fallback
Personal-number fallback
dmPolicy: "allowlist", allowFrom including your own number, selfChatMode: true. Runtime self-chat protections key off the linked self number plus allowFrom.Runtime model
- The gateway owns the WhatsApp socket and reconnect loop.
- A watchdog tracks two signals independently: raw WhatsApp Web transport activity and application-message activity. A quiet-but-connected session is not restarted just because no message arrived recently; it forces reconnect only when transport frames stop arriving for a fixed internal window (not user-configurable) or application messages stay silent past 4x the normal message timeout. Right after a reconnect for a recently active session, that first window uses the shorter normal message timeout instead of the 4x window. OpenClaw can auto-reply to offline messages that Baileys delivers early in that reconnect, bounded by the inbound message-ID dedupe lifetime; initial startup keeps the short stale-history guard.
- Baileys socket timings are explicit under
web.whatsapp.*:keepAliveIntervalMs(application ping interval),connectTimeoutMs(opening handshake timeout),defaultQueryTimeoutMs(Baileys query waits, plus OpenClaw’s outbound send/presence and inbound read-receipt timeouts). - Outbound sends require an active WhatsApp listener for the target account; sends fail fast otherwise.
- Group sends attach native mention metadata for
@+<digits>and@<digits>tokens (in text and media captions) when the token matches current participant metadata, including LID-backed groups. - Status and broadcast chats (
@status,@broadcast) are ignored. - Direct chats use DM session rules (
session.dmScope; defaultmaincollapses DMs into the agent main session). Group sessions are isolated per JID (agent:<agentId>:whatsapp:group:<jid>). - WhatsApp Channels/Newsletters can be explicit outbound targets via their native
@newsletterJID, using channel session metadata (agent:<agentId>:whatsapp:channel:<jid>) rather than DM semantics. - WhatsApp Web transport honors standard proxy environment variables on the gateway host (
HTTPS_PROXY,HTTP_PROXY,NO_PROXY, lowercase variants). Prefer host-level proxy config over per-channel settings. - With
messages.removeAckAfterReplyenabled, OpenClaw clears the ack reaction once a visible reply is delivered.
Call the current requester with MeowCaller (experimental)
The plugin can exposewhatsapp_call in WhatsApp-originated agent turns. It uses MeowCaller to place a WhatsApp voice call to the current authorized requester and play an OpenClaw TTS message after they answer. The tool has no destination-number parameter, so a prompt cannot redirect the call. Disabled by default.
Enable experimental calls
actions.calls: true to the WhatsApp channel config and restart the gateway:false, OpenClaw does not expose the whatsapp_call tool.Install the reviewed MeowCaller CLI
meowcaller executable on the gateway host’s PATH. Until MeowCaller PR #7 merges, build the reviewed branch:$HOME/.local/bin is on the gateway service’s PATH. This revision has explicit pair and send-only notify commands; notify opens no microphone, speaker, video device, or diagnostic capture. Do not substitute the upstream example CLI’s play command.Pair the MeowCaller linked device
whatsapp_call status action reports the account-specific state directory and pairing command). For the default account:MeowCaller linked device ready. Keep wa-voip.db private — it is the MeowCaller session. Non-default accounts get their own store path from the status action; on Windows, run its PowerShell command.Configure TTS and call from WhatsApp
Call me and say the build finished. The tool resolves the sender from trusted inbound context, synthesizes a temporary private WAV file, runs MeowCaller for a bounded call window, and deletes the audio file afterward. OpenClaw passes the account’s store explicitly, waits for a zero exit status after answer/playback/hangup, and treats a timeout or nonzero exit as a failed tool call.Approval prompts
WhatsApp can render exec and plugin approval prompts as👍/👎 reactions, controlled by the top-level approval forwarding config:
approvals.exec and approvals.plugin are independent; enabling WhatsApp as a channel only links the transport and sends nothing unless the matching approval family is enabled and routed there. Session mode delivers native emoji approvals only for approvals that originate from WhatsApp. Target mode uses the shared forwarding pipeline for explicit targets and does not create separate approver-DM fanout.
WhatsApp approval reactions require explicit approvers in allowFrom (or "*"). defaultTo sets ordinary default message targets, not an approver list. Manual /approve commands still pass the normal WhatsApp sender-authorization path before approval resolution.
Plugin hooks and privacy
Inbound WhatsApp messages can carry personal content, phone numbers, group identifiers, sender names, and session correlation fields. WhatsApp does not broadcast inboundmessage_received hook payloads to plugins unless you opt in:
channels.whatsapp.accounts.<id>.pluginHooks.messageReceived. Only enable this for plugins you trust with inbound WhatsApp content and identifiers.
Access control and activation
- DM policy
- Group policy and allowlists
- Mentions and /activation
channels.whatsapp.dmPolicy:| Value | Behavior |
|---|---|
pairing (default) | Unknown senders request pairing; owner approves |
allowlist | Only allowFrom senders admitted |
open | Requires allowFrom to include "*" |
disabled | Block all DMs |
allowFrom accepts E.164-style numbers (normalized internally). It is a DM sender access-control list only — it does not gate explicit outbound sends to group JIDs or @newsletter channel JIDs.Multi-account override: channels.whatsapp.accounts.<id>.dmPolicy (and .allowFrom) take precedence over channel-level defaults for that account.Runtime notes:- pairings persist in the channel allow-store and merge with configured
allowFrom - scheduled automation and heartbeat recipient fallback use explicit delivery targets or configured
allowFrom; DM pairing approvals are not implicit cron/heartbeat recipients - if no allowlist is configured, the linked self number is allowed by default
- OpenClaw never auto-pairs outbound
fromMeDMs (messages you send yourself from the linked device)
Configured ACP bindings
WhatsApp supports persistent ACP bindings via top-levelbindings[]:
Personal-number and self-chat behavior
When the linked self number is also present inallowFrom, self-chat safeguards activate: skip read receipts for self-chat turns, ignore mention-JID auto-trigger behavior that would ping yourself, and default replies to [{identity.name}] (or [openclaw]) when messages.responsePrefix is unset.
Message normalization and context
Inbound envelope and reply context
Inbound envelope and reply context
ReplyToId, ReplyToBody, ReplyToSender, sender JID/E.164) is populated when available. If the quoted target is downloadable media, OpenClaw saves it through the normal inbound media store and exposes MediaPath/MediaType so the agent can inspect it directly instead of seeing only <media:image>.Media placeholders and location/contact extraction
Media placeholders and location/contact extraction
<media:image>, <media:video>, <media:audio>, <media:document>, <media:sticker>.Authorized group voice notes are transcribed before mention gating when the body is only <media:audio>, so saying the bot mention in the voice note can trigger the reply. If the transcript still does not mention the bot, it stays in pending group history instead of the raw placeholder.Location bodies render as terse coordinate text. Location labels/comments and contact/vCard details render as fenced untrusted metadata, not inline prompt text.Pending group history injection
Pending group history injection
- default limit:
50 - config:
channels.whatsapp.historyLimit, fallbackmessages.groupChat.historyLimit 0disables
[Chat messages since your last reply - for context] and [Current message - respond to this].Read receipts
Read receipts
channels.whatsapp.accounts.<id>.sendReadReceipts. Self-chat turns skip read receipts even when globally enabled.Delivery, chunking, and media
Text chunking
Text chunking
- default chunk limit:
channels.whatsapp.textChunkLimit = 4000 channels.whatsapp.chunkMode = "length" | "newline";newlineprefers paragraph boundaries (blank lines), then falls back to length-safe chunking
Outbound media behavior
Outbound media behavior
- supports image, video, audio (PTT voice-note), and document payloads
- audio is sent as the Baileys
audiopayload withptt: true, rendering as a push-to-talk voice note;audioAsVoiceis preserved on reply payloads so TTS voice-note output stays on this path regardless of the provider’s source format - native Ogg/Opus audio sends as
audio/ogg; codecs=opus; anything else (including Microsoft Edge TTS MP3/WebM output) is transcoded withffmpegto 48 kHz mono Ogg/Opus before PTT delivery /tts latestsends the latest assistant reply as one voice note and suppresses repeat sends for the same reply;/tts chat on|off|defaultcontrols auto-TTS for the current chatgifPlayback: trueon video sends enables animated GIF playbackforceDocument/asDocumentroutes outbound images, GIFs, and videos through the Baileys document payload to avoid WhatsApp’s media compression, preserving the resolved filename and MIME type- captions apply to the first media item in a multi-media reply, except PTT voice notes: the audio sends first with no caption, then the caption sends as a separate text message (WhatsApp clients do not render voice-note captions consistently)
- media source can be HTTP(S),
file://, or a local path
Media size limits and fallback behavior
Media size limits and fallback behavior
- inbound save cap and outbound send cap:
channels.whatsapp.mediaMaxMb(default50) - per-account override:
channels.whatsapp.accounts.<id>.mediaMaxMb - images auto-optimize (resize/quality sweep) to fit limits unless
forceDocument/asDocumentrequests document delivery - on media send failure, the first-item fallback sends a text warning instead of dropping the response silently
Reply quoting
channels.whatsapp.replyToMode controls native reply quoting (outbound replies visibly quote the inbound message):
| Value | Behavior |
|---|---|
"off" (default) | Never quote; send as a plain message |
"first" | Quote only the first outbound reply chunk |
"all" | Quote every outbound reply chunk |
"batched" | Quote queued batched replies; leave immediate replies unquoted |
channels.whatsapp.accounts.<id>.replyToMode.
Reaction level
channels.whatsapp.reactionLevel controls how broadly the agent uses emoji reactions:
| Level | Ack reactions | Agent-initiated reactions |
|---|---|---|
"off" | No | No |
"ack" | Yes | No |
"minimal" (default) | Yes | Yes, conservative guidance |
"extensive" | Yes | Yes, encouraged guidance |
channels.whatsapp.accounts.<id>.reactionLevel.
Acknowledgment reactions
channels.whatsapp.ackReaction sends an immediate reaction on inbound receipt, gated by reactionLevel (suppressed when "off"):
ackReaction is present without emoji, WhatsApp uses the routed agent’s identity emoji falling back to ”👀” (omit ackReaction or set emoji: "" for no ack); failures are logged but do not block reply delivery; group mode mentions reacts only on mention-triggered turns, while group activation always bypasses that check; WhatsApp uses channels.whatsapp.ackReaction only (legacy messages.ackReaction does not apply here).
Lifecycle status reactions
Setmessages.statusReactions.enabled: true to let WhatsApp replace the ack reaction during a turn instead of leaving a static receipt emoji, cycling through states such as queued, thinking, tool activity, compaction, done, and error:
channels.whatsapp.ackReaction still controls eligibility for direct messages and groups; the queued state uses the same effective emoji as plain ack reactions; WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place; messages.removeAckAfterReply: true clears the final status reaction after the configured done/error hold; tool emoji categories include tool, coding, web, deploy, build, and concierge.
Multi-account and credentials
Account selection and defaults
Account selection and defaults
channels.whatsapp.accounts. Default account selection is default if present, otherwise the first configured account id (alphabetically sorted). Account ids are normalized internally for lookup.Credential paths and legacy compatibility
Credential paths and legacy compatibility
- current auth path:
~/.openclaw/credentials/whatsapp/<accountId>/creds.json(backup:creds.json.bak) - legacy default auth in
~/.openclaw/credentials/is still recognized/migrated for default-account flows
Logout behavior
Logout behavior
openclaw channels logout --channel whatsapp [--account <id>] clears WhatsApp auth state for that account. When a gateway is reachable, logout stops the live listener for that account first, so the linked session stops receiving messages before the next restart. openclaw channels remove --channel whatsapp also stops the live listener before disabling or deleting account config.In legacy auth directories, oauth.json is preserved while Baileys auth files are removed.Tools, actions, and config writes
- Agent tool support includes the WhatsApp reaction action (
react). - Action gates:
channels.whatsapp.actions.reactions,channels.whatsapp.actions.polls(existing actions default totrue),channels.whatsapp.actions.calls(defaultfalse, see MeowCaller above). - Channel-initiated config writes are enabled by default; disable via
channels.whatsapp.configWrites: false.
Troubleshooting
Not linked (QR required)
Not linked (QR required)
Linked but disconnected / reconnect loop
Linked but disconnected / reconnect loop
status=408 Request Time-out Connection was lost, tune Baileys socket timings under web.whatsapp. Start by shortening keepAliveIntervalMs below your network’s idle timeout and increasing connectTimeoutMs on slow or lossy links:~/.openclaw/logs/whatsapp-health.log says Gateway inactive but openclaw gateway status and openclaw channels status --probe both show healthy, run openclaw doctor. On Linux, doctor warns about legacy crontab entries invoking the retired ~/.openclaw/bin/ensure-whatsapp.sh script; remove those entries with crontab -e — cron can lack the systemd user-bus environment and make that old script misreport gateway health.QR login times out behind a proxy
QR login times out behind a proxy
openclaw channels login --channel whatsapp fails before showing a usable QR with status=408 Request Time-out or a TLS socket disconnect.WhatsApp Web login uses the gateway host’s standard proxy environment (HTTPS_PROXY, HTTP_PROXY, lowercase variants, NO_PROXY). Verify the gateway process inherits the proxy env and that NO_PROXY does not match mmg.whatsapp.net.No active listener when sending
No active listener when sending
Group messages unexpectedly ignored
Group messages unexpectedly ignored
groupPolicy, groupAllowFrom/allowFrom, groups allowlist entries, mention gating (requireMention + mention patterns), and duplicate keys in openclaw.json (JSON5 later entries override earlier ones — keep a single groupPolicy per scope).If channels.whatsapp.groups is present, WhatsApp can still observe messages from other groups, but OpenClaw drops them before session routing. Add the group JID to channels.whatsapp.groups, or add groups["*"] to admit all groups while keeping sender authorization under groupPolicy/groupAllowFrom.Bun runtime warning
Bun runtime warning
System prompts
WhatsApp supports Telegram-style system prompts for groups and direct chats via thegroups and direct maps.
Resolution for group messages: the effective groups map is determined first — if the account defines its own groups key at all, it fully replaces the root groups map (no deep merge). Prompt lookup then runs on that single resulting map:
- Group-specific prompt (
groups["<groupId>"].systemPrompt): used when the group entry exists and itssystemPromptkey is defined. An empty string ("") suppresses the wildcard and applies no prompt. - Group wildcard prompt (
groups["*"].systemPrompt): used when the specific group entry is absent, or exists without asystemPromptkey.
direct map and direct["*"].
dms remains the lightweight per-DM history override bucket (dms.<id>.historyLimit). Prompt overrides live under direct.groups/direct key, including an explicit empty object, replaces the root map. It differs from the group-membership allowlist check described above, which has a single-account safety net for an accidentally empty groups: {}.groups for every account in a multi-account setup (even accounts with no groups of their own) to stop a bot receiving group messages for groups it does not belong to. WhatsApp does not apply that guard — root groups/direct are inherited by any account without its own override, regardless of account count. In a multi-account WhatsApp setup, define the full map under each account explicitly if you want per-account prompts.
Important behavior:
channels.whatsapp.groupsis both a per-group config map and the chat-level group allowlist. At either root or account scope,groups["*"]means “all groups are admitted” for that scope.- Only add a wildcard
systemPromptwhen you already want that scope to admit all groups. To keep only a fixed set of group IDs eligible, repeat the prompt on each explicitly allowlisted entry instead of usinggroups["*"]. - Group admission and sender authorization are separate checks.
groups["*"]widens which groups reach group handling; it does not authorize every sender in those groups — that stays controlled bygroupPolicy/groupAllowFrom. channels.whatsapp.directhas no equivalent side effect for DMs:direct["*"]only supplies a default config after a DM is already admitted bydmPolicyplusallowFromor pairing-store rules.
Configuration reference pointers
Primary reference: Configuration reference - WhatsApp| Area | Fields |
|---|---|
| Access | dmPolicy, allowFrom, groupPolicy, groupAllowFrom, groups |
| Delivery | textChunkLimit, chunkMode, mediaMaxMb, sendReadReceipts, ackReaction, reactionLevel |
| Multi-account | accounts.<id>.enabled, accounts.<id>.authDir, and other per-account overrides |
| Operations | configWrites, debounceMs, web.enabled, web.heartbeatSeconds, web.reconnect.*, web.whatsapp.* |
| Session behavior | session.dmScope, historyLimit, dmHistoryLimit, dms.<id>.historyLimit |
| Prompts | groups.<id>.systemPrompt, groups["*"].systemPrompt, direct.<id>.systemPrompt, direct["*"].systemPrompt |