WhatsApp Channel: Setup, Access Controls, and Delivery Behavior

Covers WhatsApp channel support through WhatsApp Web (Baileys), including plugin install, pairing, access controls, delivery behavior, and operations. Useful for operators configuring or troubleshooting the gateway-owned WhatsApp session.

Read this when

  • Working on WhatsApp/web channel behavior or inbox routing

Status: production-ready through WhatsApp Web (Baileys). The linked session(s) are owned by the gateway; a separate Twilio WhatsApp channel does not exist.

Setup

Install

The first time you pick openclaw onboard or openclaw channels add --channel whatsapp, you are prompted to install the plugin; openclaw channels login --channel whatsapp provides the same install flow when the plugin is absent. Dev checkouts point at the local plugin path; for stable/beta installs, @openclaw/whatsapp tries npm first and only falls back to the declared ClawHub package when the npm target cannot be reached. Because the WhatsApp runtime is not part of the core OpenClaw npm package, its runtime dependencies remain bundled with the external plugin. Manual install:

openclaw plugins install @openclaw/whatsapp

By default, the manual package install goes through ClawHub. To install straight from npm, use npm:@openclaw/whatsapp; pin an exact version only when you need a reproducible install.

Quick setup

Configure access policy

{
  channels: {
    whatsapp: {
      dmPolicy: "pairing",
      allowFrom: ["+15551234567"],
      groupPolicy: "allowlist",
      groupAllowFrom: ["+15551234567"],
    },
  },
}
openclaw channels login --channel whatsapp

Login works by QR code only. If the host is remote or headless, make sure a reliable path exists for getting the live QR to the phone before login begins; QRs rendered in a terminal, screenshots, or chat attachments may expire along the way.

For one specific account:

openclaw channels login --channel whatsapp --account work

To attach an existing or custom auth directory prior to login:

openclaw channels add --channel whatsapp --account work --auth-dir /path/to/wa-auth
openclaw channels login --channel whatsapp --account work

Start the gateway

openclaw gateway

Approve the first DM access request (pairing mode)

Go to Settings โ†’ Channels โ†’ DM access requests, locate the WhatsApp account, then approve the sender. If you would rather use the CLI:

openclaw pairing list whatsapp
openclaw pairing approve whatsapp <CODE>

DM access requests lapse after 1 hour, and each account holds at most 3 pending requests. This approval is distinct from the WhatsApp login QR that links the account itself.

Note

Using a separate WhatsApp number is recommended (setup and metadata are tuned for that), though personal-number/self-chat setups are fully supported.

Deployment patterns

Dedicated number (recommended)

  • a distinct WhatsApp identity for OpenClaw
  • DM allowlists and routing boundaries that are easier to follow
  • less risk of self-chat confusion
{
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551234567"],
    },
  },
}

Personal-number fallback

Personal-number mode is supported during onboarding, which writes a self-chat-friendly baseline: dmPolicy: "allowlist", allowFrom with your own number included, selfChatMode: true. Admission and safeguard rules are covered in Personal-number and self-chat behavior.

Runtime model

  • The WhatsApp socket and its reconnect loop are owned by the gateway.
  • Two signals are tracked independently by a watchdog: raw WhatsApp Web transport activity and application-message activity. A session that is connected but quiet is not restarted merely because no message arrived lately; a forced reconnect happens only when transport frames stop arriving for a fixed internal window (this is not user-configurable) or when application messages remain silent beyond 4x the normal message timeout. Immediately after a reconnect for a session that was recently active, that first window uses the shorter normal message timeout rather than the 4x window. OpenClaw may auto-reply to offline messages that Baileys delivers early in that reconnect, limited by the inbound message-ID dedupe lifetime; at initial startup the short stale-history guard still applies.
  • For outbound sends, an active WhatsApp listener is required for the target account; otherwise sends fail fast.
  • When the token matches current participant metadata, including LID-backed groups, group sends attach native mention metadata for @+<digits> and @<digits> tokens (in text and in media captions).
  • Status and broadcast chats (@status, @broadcast) are skipped.
  • DM session rules govern direct chats (session.dmScope; the default main collapses DMs into the agent main session). Under the default session.groupScope: "per-group", group sessions stay isolated per JID (agent:<agentId>:whatsapp:group:<jid>).
  • WhatsApp Channels/Newsletters can be targeted explicitly for outbound via their native @newsletter JID, which uses channel session metadata (agent:<agentId>:whatsapp:channel:<jid>) instead of DM semantics.
  • Standard proxy environment variables on the gateway host are honored by the WhatsApp Web transport (HTTPS_PROXY, HTTP_PROXY, NO_PROXY, plus lowercase variants). Host-level proxy config is preferable to per-channel settings.
  • On Node and Bun, media uploads share the same proxy environment and managed HTTPS-proxy trust, and NO_PROXY is evaluated for every actual upload and redirect host independently of the WebSocket destination.
  • Media proxy URLs have to use HTTP or HTTPS. If media proxy settings are invalid, uploads fail while login and text messaging keep working.

Call the current requester with MeowCaller (experimental)

In agent turns that originate from WhatsApp, the plugin can expose whatsapp_call. It relies on MeowCaller to place a WhatsApp voice call to the current authorized requester and, once they answer, play an OpenClaw TTS message. No destination-number parameter exists on the tool, so a prompt cannot redirect the call. Off by default.

Warning

MeowCaller is experimental, ships no tagged release, and relies on a separately paired whatsmeow linked-device session, so it cannot reuse the plugin's Baileys credentials. Pairing links one more device to the same WhatsApp account; scan using the identity OpenClaw uses. In personal-number/self-chat mode the account cannot call itself; to call your personal number, use a dedicated OpenClaw number.

Enable experimental calls

Put actions.calls: true into the WhatsApp channel config. Changes are picked up by hot reload:

{
  "channels": {
    "whatsapp": {
      "actions": {
        "calls": true
      }
    }
  }
}

If the setting is missing or set to false, the whatsapp_call tool is not exposed by OpenClaw.

Install the reviewed MeowCaller CLI

A meowcaller executable must be present on the gateway host's PATH for the adapter to work. Until MeowCaller PR #7 is merged, build the reviewed branch:

git clone --branch feat/send-only-notify https://github.com/steipete/meowcaller.git
cd meowcaller
git checkout 752050471fc2bf7a8cdfbf7dbd3cd4e865d85d3f
mkdir -p "$HOME/.local/bin"
go build -o "$HOME/.local/bin/meowcaller" ./cmd/meowcaller

Put $HOME/.local/bin into the gateway service's PATH; if you altered the service environment, restart the Gateway. This revision provides dedicated pair and send-only notify commands; notify opens no microphone, speaker, video device, or diagnostic capture. Never swap in the play command from the upstream example CLI.

Pair the MeowCaller linked device

Have the WhatsApp agent verify call setup (the whatsapp_call status action returns the account-specific state directory and the pairing command). For the default account:

state_dir="$HOME/.openclaw/credentials/whatsapp-calls/default"
mkdir -p "$state_dir"
chmod 700 "$state_dir"
meowcaller pair --store "$state_dir/wa-voip.db"

Run it interactively, scan the QR code under WhatsApp > Linked devices, then wait for MeowCaller linked device ready. Treat wa-voip.db as private: it is the MeowCaller session. Accounts other than the default receive their own store path from the status action; on Windows, run its PowerShell command.

Configure TTS and call from WhatsApp

Set up a telephony-capable TTS provider, then issue a request like Call me and say the build finished.. The sender is resolved by the tool from trusted inbound context, a temporary private WAV file is synthesized, MeowCaller runs for a bounded call window, and the audio file is removed afterward. OpenClaw passes the account's store explicitly, waits for a zero exit status after answer/playback/hangup, and counts a timeout or nonzero exit as a failed tool call.

Limits: outbound audio calls are one-to-one only, arbitrary destination numbers are not allowed, auth is not shared with the chat connection, self-calls from personal-number/self-chat mode are excluded, synthesized audio is capped at 60 seconds, no handset-side audibility receipt exists beyond MeowCaller's answer/playback/hangup completion, and OpenClaw terminates the companion process after a bounded 115-175 second window (covering MeowCaller's connection, answer, playback, and shutdown phases).

Approval prompts

Exec and plugin approval prompts can be rendered by WhatsApp as ๐Ÿ‘/๐Ÿ‘Ž reactions, governed by the top-level approval forwarding config:

{
  approvals: {
    exec: {
      enabled: true,
      mode: "session",
    },
    plugin: {
      enabled: true,
      mode: "targets",
      targets: [{ channel: "whatsapp", to: "+15551234567" }],
    },
  },
}

approvals.exec and approvals.plugin operate independently; making WhatsApp a channel only links the transport and sends nothing unless the matching approval family is enabled and routed there. In session mode, native emoji approvals are delivered only for approvals that originate from WhatsApp. Target mode routes explicit targets through the shared forwarding pipeline and does not create separate approver-DM fanout.

WhatsApp approval reactions need explicit approvers in allowFrom (or "*"). defaultTo defines ordinary default message targets, not an approver list. Manual /approve commands still pass the normal WhatsApp sender-authorization path before approval resolution.

Agent questions

The ask_user tool is not exposed by WhatsApp. When your input is needed, the agent asks in an ordinary message and you answer in plain text. Numbered reactions do not answer questions. Approval reactions described above are unchanged.

Plugin hooks and privacy

Personal content, phone numbers, group identifiers, sender names, and session correlation fields may all appear in inbound WhatsApp messages. Inbound message_received hook payloads are not broadcast by WhatsApp to plugins unless you opt in:

{
  channels: {
    whatsapp: {
      pluginHooks: {
        messageReceived: true,
      },
    },
  },
}

Scope the opt-in to a single account under channels.whatsapp.accounts.<id>.pluginHooks.messageReceived. Enable this only for plugins you trust with inbound WhatsApp content and identifiers.

Access control

Access control and activation

DM policy

channels.whatsapp.dmPolicy:

ValueBehavior
pairing (default)Unknown senders request pairing; owner approves
allowlistOnly allowFrom senders admitted
openRequires allowFrom to include "*"
disabledBlock all DMs

allowFrom accepts E.164-style numbers (normalized internally). It serves solely as a DM sender access-control list, it does not gate explicit outbound sends to group JIDs or @newsletter channel JIDs.

Multi-account override: for that account, channels.whatsapp.accounts.<id>.dmPolicy (and .allowFrom) take precedence over channel-level defaults.

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
  • same-number self-DMs are allowed unless selfChatMode: false or dmPolicy: "disabled"; see Self-chat behavior
  • outbound fromMe DMs (messages you send yourself from the linked device) are never auto-paired by OpenClaw

Group policy and allowlists

Group access has two layers:

  1. Group membership allowlist (channels.whatsapp.groups): when groups is left out, every group qualifies; when it is supplied, it serves as a group allowlist ("*" admits all).
  2. Group sender policy (channels.whatsapp.groupPolicy + groupAllowFrom): open skips the sender allowlist, allowlist demands a groupAllowFrom (or *) match, and disabled rejects all inbound group traffic.

When groupAllowFrom is not set, sender checks revert to allowFrom provided it contains entries. Evaluation of sender allowlists happens ahead of mention/reply activation.

Should no channels.whatsapp block be present, the runtime reverts to groupPolicy: "allowlist" (logging a warning), regardless of whether channels.defaults.groupPolicy holds a different value.

Note

Inbound group handling relies on channels.whatsapp.accounts.<id>.groups when that is set. Otherwise, named accounts inherit channels.whatsapp.accounts.default.groups, then channels.whatsapp.groups; the default account draws on its own map or the root map. Group maps are replaced wholesale rather than merged entry by entry. A map that is explicitly empty ({}) likewise replaces the inherited map.

Mentions and /activation

By default, a group reply needs a mention. Mention detection covers:

  • direct WhatsApp mentions of the bot identity
  • mention regex patterns (agents.entries.*.groupChat.mentionPatterns, falling back to messages.groupChat.mentionPatterns); if neither exists, patterns come from the routed agent's identity.name and identity.emoji
  • transcripts of inbound voice notes for authorized group messages
  • implicit reply-to-bot detection (the reply sender matches the bot identity)

Setting mentionPatterns: [] explicitly at the selected agent or global level turns off text patterns derived from identity. Native mentions and reply-to-bot detection stay independent of that.

To handle every allowed message in a group, place groups["<group-id>"].requireMention: false in whichever map already provides the account's group policy: channels.whatsapp.groups or the effective channels.whatsapp.accounts.<id>.groups map, inherited accounts.default.groups included. Keep all existing wildcard and per-group settings intact. If you choose to build an account-specific map rather than edit an inherited one, copy the entire inherited map first, then alter only the target group's requireMention. Where no map applied before, add "*": {} so other chats stay admitted; otherwise keep the restrictions already in place. A saved session activation mode overrides this config default, so use /activation always for that session.

Security: quoting or replying only clears the mention gate. It does not authorize the sender. With groupPolicy: "allowlist", senders who are not allowlisted remain blocked, even when they reply to an allowlisted user's message.

Session-level activation command: /activation mention or /activation always. It changes session state rather than global config, and only the owner can run it.

Named accounts rely solely on their own account-scoped activation. An older unscoped group's preference is no longer inherited. If that was the only preference you saved, the configured mention policy takes effect until you run /activation again in the intended account's group. Existing scoped and default-account preferences, session history, and stored rows stay as they are; nothing migrates the older setting automatically.

Configured ACP bindings

Persistent ACP bindings are available in WhatsApp through the top-level bindings[]:

{
  bindings: [
    {
      type: "acp",
      agentId: "codex",
      match: {
        channel: "whatsapp",
        accountId: "work",
        peer: { kind: "direct", id: "+15555550123" },
      },
    },
    {
      type: "acp",
      agentId: "codex",
      match: {
        channel: "whatsapp",
        accountId: "work",
        peer: { kind: "group", id: "120363424282127706@g.us" },
      },
    },
  ],
}

E.164 numbers are what direct chats match; WhatsApp group JIDs are what groups match. Before OpenClaw creates the bound ACP session (if one is needed), group allowlists, sender policy, and mention/activation gating all run. Once a binding matches, it owns the route, so broadcast groups do not spread that turn to ordinary WhatsApp sessions.

Personal-number and self-chat behavior

channels.whatsapp.selfChatMode governs same-number DM admission and self-chat safeguards. Configure it at the channel level, or override it for a single account with channels.whatsapp.accounts.<id>.selfChatMode.

  • true or unset: DMs sent from the linked number to itself are admitted as agent input, even if that number is missing from allowFrom. dmPolicy: "disabled" continues to block every DM.
  • false: DMs originating from yourself are ignored, even when your number is listed in allowFrom. Other inbound DMs and group messages still obey their access policies; the account does not become outbound-only because of this setting.

The implicit self-number allowance covers DMs only, never group allowlists.

Self-chat safeguards turn on with true and turn off with false. If the setting is unset, OpenClaw enables them when the linked self number shows up in the configured allowFrom. These safeguards skip read receipts, suppress native self-mention triggers, and provide an identity reply prefix when no response prefix has been configured.

So a liveness check sent to your own number can become agent input while selfChatMode is unset or true. Set selfChatMode: false if you would rather exclude those self-originated DMs.

Messaging and delivery

Message normalization and context

Inbound envelope and reply context

The shared inbound envelope wraps incoming messages. A quoted reply appends context as follows:

[Replying to <sender> id:<stanzaId>]
<quoted body or media placeholder>
[/Replying]

When available, reply metadata (ReplyToId, ReplyToBody, ReplyToSender, sender JID/E.164) is filled in. Should the quoted target be downloadable media, OpenClaw stores it via the standard inbound media store and surfaces MediaPath/MediaType, letting the agent examine it directly rather than receiving only <media:image>.

Media placeholders and location/contact extraction

Messages that contain media alone are normalized into placeholders: <media:image>, <media:video>, <media:audio>, <media:document>, <media:sticker>.

For authorized groups, voice notes get transcribed ahead of mention gating whenever the body consists solely of <media:audio>, which means speaking the bot mention inside the voice note can trigger a reply. Where the transcript still contains no mention of the bot, the message remains in pending group history rather than the raw placeholder.

Location bodies are rendered as short coordinate text. Fenced untrusted metadata, not inline prompt text, is used for location labels/comments and contact/vCard details.

Pending group history injection

Group messages that have not yet been processed are buffered and injected as context once the bot is eventually triggered.

  • default limit: 50
  • config: channels.whatsapp.historyLimit, fallback messages.groupChat.historyLimit
  • 0 disables

Injection markers: [Chat messages since your last reply - for context] and [Current message - respond to this].

Read receipts

Accepted inbound messages get this by default. To turn it off globally:

{ channels: { whatsapp: { sendReadReceipts: false } } }

Per-account override: channels.whatsapp.accounts.<id>.sendReadReceipts. Even with the global setting on, self-chat safeguards cause read receipts to be skipped (see Self-chat behavior).

Delivery, chunking, and media

Text chunking

  • default chunk limit: channels.whatsapp.textChunkLimit = 4000
  • conversion of Markdown happens before any chunk is sent, so styles survive message boundaries and WhatsApp formatting markers count toward the limit
  • channels.whatsapp.streaming.chunkMode = "length" | "newline"; newline favors paragraph boundaries (blank lines) first, then drops back to length-safe chunking

Outbound media behavior

  • image, video, audio (PTT voice-note), and document payloads are all supported
  • a message-tool send that succeeds in the current chat counts as the reply for text and attachments alike, contacts with a known LID-to-phone mapping included; the agent may finish with NO_REPLY and no false failure notice appears
  • audio goes out as the Baileys audio payload carrying ptt: true, which renders as a push-to-talk voice note; audioAsVoice survives on reply payloads, keeping TTS voice-note output on this path whatever source format the provider uses
  • native Ogg/Opus audio is sent as audio/ogg; codecs=opus; all other audio, Microsoft Edge TTS MP3/WebM output included, is transcoded by ffmpeg to 48 kHz mono Ogg/Opus prior to PTT delivery
  • /tts latest delivers the latest assistant reply as a single voice note and blocks repeat sends of that same reply; auto-TTS for the current chat is governed by /tts chat on|off|default
  • with gifPlayback: true set on video sends, animated GIF playback is enabled
  • forceDocument/asDocument send outbound images, GIFs, and videos through the Baileys document payload, sidestepping WhatsApp's media compression while keeping the resolved filename and MIME type
  • in a multi-media reply, captions attach to the first media item, PTT voice notes excepted: the audio is sent first without a caption, then the caption follows as its own text message (WhatsApp clients do not render voice-note captions consistently)
  • if a later captioned reply repeats pending attachments, only the attachments WhatsApp accepts replace their pending copies; the rest stay queued for delivery
  • the media source may be HTTP(S), file://, or a local path

Media size limits and fallback behavior

  • inbound save cap and outbound send cap: channels.whatsapp.mediaMaxMb (default 50)
  • per-account override: channels.whatsapp.accounts.<id>.mediaMaxMb
  • to fit the limits, images are auto-optimized (resize/quality sweep) unless forceDocument/asDocument asks for document delivery
  • if a media send fails, the first-item fallback emits a text warning rather than silently dropping the response

Reply quoting

Native reply quoting (outbound replies visibly quote the inbound message) is controlled by channels.whatsapp.replyToMode:

ValueBehavior
"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

When the original message text and media details are no longer in the cache, OpenClaw falls back to sending a plain message. The reply body itself is kept, though its visual link to the original message is gone.

Per-account override: channels.whatsapp.accounts.<id>.replyToMode.

{ channels: { whatsapp: { replyToMode: "first" } } }

Reactions and typing

Reaction level

How broadly the agent applies emoji reactions is set by channels.whatsapp.reactionLevel:

LevelAck reactionsAgent-initiated reactions
"off"NoNo
"ack"YesNo
"minimal" (default)YesYes, conservative guidance
"extensive"YesYes, encouraged guidance

Per-account override: channels.whatsapp.accounts.<id>.reactionLevel.

{ channels: { whatsapp: { reactionLevel: "ack" } } }

Acknowledgment reactions

Once an inbound message is accepted, messages.ackReaction fires a reaction right away. The active WhatsApp account's reactionLevel governs this (it is suppressed when "off" is set). Which chats get the reaction is chosen by messages.ackReactionScope: direct messages, groups, or both:

{
  messages: {
    ackReaction: "๐Ÿ‘€",
    ackReactionScope: "group-mentions", // all | direct | group-all | group-mentions | off
  },
}

Notes: the reaction goes out as soon as the inbound message is accepted, before any reply. To turn acknowledgment off, leave messages.ackReaction out or assign it "". Delivery of the reply is never blocked by a failure, though failures do get logged. "group-mentions" is the default scope; "all" covers direct messages plus every eligible group. When a group's activation is always, "group-mentions" acknowledges each message instead of only mention-triggered turns, since activation takes the place of the mention check.

Lifecycle status reactions

With messages.statusReactions.enabled: true set, WhatsApp swaps the ack reaction for changing states during a turn rather than leaving one static receipt emoji. The states it cycles through include queued, thinking, tool activity, compaction, done, and error:

{
  messages: {
    statusReactions: {
      enabled: true,
    },
  },
}

Notes: eligibility for direct messages and groups is still determined by messages.ackReactionScope; the queued state reuses the same effective emoji that plain acknowledgment reactions use. Each WhatsApp message has a single bot reaction slot, so lifecycle updates overwrite the reaction in place, and the acknowledgment is restored once the final done/error state is reached.

A message waiting behind an active run keeps its acknowledgment until its follow-up produces a visible reply. A success reaction is not generated merely because execution succeeded. Where transport holds a delivery that is still unconfirmed, the reaction is cleared without asserting either success or failure; durable retries that happen later leave that cleared reaction as is. A turn that settles with no visible reply, including a silent turn or one marked NO_REPLY, displays an error reaction, the same as direct dispatch. An error reaction appears immediately for execution failures, terminal admission skips, Stop cancellations, and messages the queue-cap policy rejects or removes. Delivery correlation is preserved per message for collected and summarized follow-ups.

Active-turn typing

When an automatic turn is admitted and typing is permitted, a composing presence update goes out as agent execution starts, and WhatsApp refreshes it for as long as the turn stays active. Once the run completes, whether by terminal failure or cancellation, refreshing ceases. The controller seals and cleans up when the reply dispatcher reports idle, or following a short safety timeout should that signal never arrive. No such activity starts for turns where the existing typing and suppression policy turns typing off.

Typing presence is ephemeral, best-effort activity feedback. It is not a persisted message, a delivery receipt, or a promise that every WhatsApp client will show continuous activity; reconnects and client behavior can cause the indicator to vanish. The persistent-looking opt-in status surface remains the lifecycle status reactions described above.

Multi-account and credentials

Account selection and defaults

channels.whatsapp.accounts supplies the account ids. If default is present it becomes the default account; otherwise the first configured account id in alphabetical order is used. Lookups normalize account ids internally.

For named accounts, shared settings resolve in this order: the account, then accounts.default, then the channel root. This covers dmPolicy and groupPolicy: an omitted value is inherited, while an explicit value takes precedence. Absent any configured policy, DMs fall back to pairing and groups to allowlist. Named accounts do not share the default account's authDir, enabled, name, and selfChatMode.

Credential paths and legacy compatibility

  • current auth path: ~/.openclaw/credentials/whatsapp/<accountId>/creds.json (backup: creds.json.bak)
  • After preserving exact private .migrated backups, Doctor relocates implicit legacy default auth from ~/.openclaw/credentials/ into the account directory. Runtime account selection reads from the account directory; if you replaced an older installation in place, run openclaw doctor --fix afterwards.
  • Settings given explicitly through authDir stay authoritative, including an account that intentionally uses the shared credentials root. Doctor leaves such directories untouched. When current and legacy credentials differ, both are kept and an actionable conflict warning is raised; Doctor never blends two credential sets.
  • Before writing the account directory, Doctor records a credential migration receipt. Should an interrupted import's complete destination later be missing or altered, Doctor keeps the remaining sources and backups and requests explicit recovery. It will not recreate an account that was logged out.
  • Credentials in the account directory continue to work on OpenClaw 2026.9.7. Repairing the shared root requires a host with offline Doctor migration authority plus source backups. If Doctor tells you to upgrade the host, update OpenClaw core before retrying; the plugin leaves the original credentials as they are.

Logout behavior

openclaw channels logout --channel whatsapp [--account <id>] wipes WhatsApp auth state for the given account. Where a gateway can be reached, logout first stops that account's live listener, so the linked session stops receiving messages ahead of the next restart. openclaw channels remove --channel whatsapp likewise halts the live listener before account config is disabled or deleted.

Logout does not touch credential directories that were explicitly configured outside the managed account tree. Unrelated OAuth credentials in the shared root are never deleted by it.

Tools, actions, and config writes

  • The WhatsApp reaction action (react) is included in agent tool support.
  • Action gates: channels.whatsapp.actions.reactions, channels.whatsapp.actions.polls (existing actions default to true), channels.whatsapp.actions.calls (default false, see MeowCaller above).
  • Config writes started by the channel are on by default; turn them off with channels.whatsapp.configWrites: false.

Troubleshooting

Denials from policy and allowlists show up in normal Gateway logs as Inbound message blocked (reason=...). For any given reason, each account listener logs at most once per minute, and no sender identifiers or message content are included.

Not linked (QR required)

Symptom: channel status reports not linked.

openclaw channels login --channel whatsapp
openclaw channels status

Linked but disconnected / reconnect loop

Symptom: the linked account keeps dropping the connection or retrying to reconnect.

An idle account may remain connected beyond the usual message timeout. The watchdog only restarts when WhatsApp Web transport activity halts, the socket closes, or application-level activity goes silent past the longer safety window (refer to Runtime model above).

Fix:

openclaw channels status --probe
openclaw doctor
openclaw logs --follow
openclaw gateway status

Should the loop continue once host connectivity and timing are corrected, back up the account auth directory and link the account again:

cp -a ~/.openclaw/credentials/whatsapp/<accountId> \
  ~/.openclaw/credentials/whatsapp/<accountId>.bak
openclaw channels logout --channel whatsapp --account <accountId>
openclaw channels login --channel whatsapp --account <accountId>

When ~/.openclaw/logs/whatsapp-health.log reports Gateway inactive while openclaw gateway status and openclaw channels status --probe are both healthy, execute openclaw doctor. On Linux, doctor flags legacy crontab entries that call the retired ~/.openclaw/bin/ensure-whatsapp.sh script. Remove those entries using crontab -e: cron may lack the systemd user-bus environment, which causes that old script to report gateway health incorrectly.

QR login times out behind a proxy

Symptom: openclaw channels login --channel whatsapp fails with status=408 Request Time-out before a usable QR appears, or a TLS socket disconnect occurs.

Login to WhatsApp Web relies on the standard proxy environment of the gateway host (HTTPS_PROXY, HTTP_PROXY, lowercase variants, NO_PROXY). Confirm the gateway process inherits the proxy env, and that NO_PROXY does not match mmg.whatsapp.net.

No active listener when sending

When no active gateway listener exists for the target account, outbound sends fail immediately. Verify the gateway is running and that the account is linked.

Reply appears in transcript but not in WhatsApp

Transcript rows capture what the agent produced; WhatsApp delivery is verified separately. OpenClaw counts an auto-reply as sent only once Baileys returns an outbound message id for at least one visible text or media send.

Ack reactions are pre-reply receipts and stand on their own: a reaction that succeeds does not show that the text/media reply sent afterward was accepted. Look in the gateway logs for auto-reply delivery failed or auto-reply was not accepted by WhatsApp provider.

Group messages unexpectedly ignored

Verify in this sequence: groupPolicy, groupAllowFrom/allowFrom, the groups allowlist entries, mention gating (requireMention plus mention patterns), and duplicate keys in openclaw.json (in JSON5, later entries override earlier ones, so keep one groupPolicy per scope).

With channels.whatsapp.groups present, WhatsApp may still see messages from other groups, yet OpenClaw discards them before session routing. Either add the group JID to channels.whatsapp.groups, or add groups["*"] so all groups are admitted while sender authorization stays under groupPolicy/groupAllowFrom.

Bun runtime warning

Node is still the primary, recommended Gateway runtime. Bun 1.4.3+ builds that include WAL-reset-safe node:sqlite are supported only as an explicit opt-in; doctor moves unsupported Bun services to Node only.

System prompts

Through the groups and direct maps, WhatsApp offers Telegram-style system prompts for groups and direct chats.

Group message resolution: first the effective groups map is settled. If the account defines its own groups key at all, that map fully replaces the root groups map (no deep merge). Prompt lookup then runs against that one resulting map:

  1. Group-specific prompt (groups["<groupId>"].systemPrompt): applies when the group entry exists and its systemPrompt key is defined. An empty string ("") suppresses the wildcard and applies no prompt.
  2. Group wildcard prompt (groups["*"].systemPrompt): applies when the specific group entry is missing, or exists without a systemPrompt key.

Direct message resolution follows the same pattern, against the direct map and direct["*"].

Note

dms stays the lightweight per-DM history override bucket (dms.<id>.historyLimit). Prompt overrides are stored under direct.

Note

For prompt resolution, this account-replaces-root behavior is a plain shallow override: any account groups/direct key, an explicit empty object included, replaces the root map. It is not the same as the group-membership allowlist check described above, which carries a single-account safety net for an accidentally empty groups: {}.

Difference from Telegram: In multi-account configs, Telegram applies the same whole-map account override to groups, but an empty groups: {} for a single account falls back to root groups as a migration safety net. Telegram's direct map also carries separate DM-topic semantics. In WhatsApp, or for one account among several Telegram accounts, use an explicit empty groups: {} when that account should not inherit root group defaults.

Behavior to keep in mind:

  • channels.whatsapp.groups serves two roles at once: a config map for each group and the chat-level allowlist of groups. Set at root or account scope, groups["*"] admits every group within that scope.
  • Add a wildcard systemPrompt only if you already intend that scope to admit all groups. If you want just a fixed set of group IDs to qualify, repeat the prompt on each entry you allowlist explicitly rather than relying on groups["*"].
  • Whether a group is admitted and whether a sender is authorized are two distinct checks. groups["*"] broadens which groups get through to group handling; it does not grant authorization to every sender inside those groups, since groupPolicy/groupAllowFrom still govern that.
  • For DMs, channels.whatsapp.direct has no matching side effect: only after a DM has already been admitted by dmPolicy plus allowFrom or the pairing-store rules does direct["*"] provide a default config.

Example:

{
  channels: {
    whatsapp: {
      groups: {
        // Use only if all groups should be admitted at the root scope.
        // Applies to all accounts that do not define their own groups map.
        "*": { systemPrompt: "Default prompt for all groups." },
      },
      direct: {
        // Applies to all accounts that do not define their own direct map.
        "*": { systemPrompt: "Default prompt for all direct chats." },
      },
      accounts: {
        work: {
          groups: {
            // This account defines its own groups, so root groups are fully
            // replaced. To keep a wildcard, define "*" explicitly here too.
            "120363406415684625@g.us": {
              requireMention: false,
              systemPrompt: "Focus on project management.",
            },
            // Use only if all groups should be admitted in this account.
            "*": { systemPrompt: "Default prompt for work groups." },
          },
          direct: {
            // This account defines its own direct map, so root direct entries are
            // fully replaced. To keep a wildcard, define "*" explicitly here too.
            "+15551234567": { systemPrompt: "Prompt for a specific work direct chat." },
            "*": { systemPrompt: "Default prompt for work direct chats." },
          },
        },
      },
    },
  },
}

Configuration reference pointers

Primary reference: Configuration reference - WhatsApp

AreaFields
AccessdmPolicy, allowFrom, groupPolicy, groupAllowFrom, groups
DeliverytextChunkLimit, streaming.chunkMode, mediaMaxMb, sendReadReceipts, reactionLevel
Multi-accountaccounts.<id>.enabled, accounts.<id>.authDir, and other per-account overrides
OperationsconfigWrites, enabled
Inbound batchingmessages.inbound.debounceMs, messages.inbound.byChannel.whatsapp
Acknowledgmentsmessages.ackReaction, messages.ackReactionScope
Session behaviorsession.dmScope, historyLimit, dmHistoryLimit, dms.<id>.historyLimit
Promptsgroups.<id>.systemPrompt, groups["*"].systemPrompt, direct.<id>.systemPrompt, direct["*"].systemPrompt
5,548 words ยท updated Oct 26, 2026