Docs / channels / whatsapp

WhatsApp

WhatsApp (web channel)

Quick setup (beginner)

  1. Use a separate phone number if possible (recommended).
  2. Configure WhatsApp in ~/.openclaw/openclaw.json.
  3. Run openclaw channels login to scan the QR code (Linked Devices).
  4. Start the gateway.
text
{
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551234567"],
    },
  },
}

Goals

  • Multiple WhatsApp accounts (multi-account) in one Gateway process.
  • Deterministic routing: replies return to WhatsApp, no model routing.
  • Model sees enough context to understand quoted replies.

Config writes

text
{
  channels: { whatsapp: { configWrites: false } },
}

Architecture (who owns what)

  • Gateway owns the Baileys socket and inbox loop.
  • CLI / macOS app talk to the gateway; no direct Baileys use.
  • Active listener is required for outbound sends; otherwise send fails fast.

Getting a phone number (two modes)

text
{
  channels: {
    whatsapp: {
      dmPolicy: "allowlist",
      allowFrom: ["+15551234567"],
    },
  },
}

Personal number (fallback)

text
{
  "whatsapp": {
    "selfChatMode": true,
    "dmPolicy": "allowlist",
    "allowFrom": ["+15551234567"]
  }
}

Number sourcing tips

  • Local eSIM from your country’s mobile carrier (most reliable)
  • Austria: hot.at
  • UK: giffgaff — free SIM, no contract
  • Prepaid SIM — cheap, just needs to receive one SMS for verification

Why Not Twilio?

  • Early OpenClaw builds supported Twilio’s WhatsApp Business integration.
  • WhatsApp Business numbers are a poor fit for a personal assistant.
  • Meta enforces a 24‑hour reply window; if you haven’t responded in the last 24 hours, the business number can’t initiate new messages.
  • High-volume or “chatty” usage triggers aggressive blocking, because business accounts aren’t meant to send dozens of personal assistant messages.
  • Result: unreliable delivery and frequent blocks, so support was removed.

Login + credentials

  • Login command: openclaw channels login (QR via Linked Devices).
  • Multi-account login: openclaw channels login --account <id> (<id> = accountId).
  • Default account (when --account is omitted): default if present, otherwise the first configured account id (sorted).
  • Credentials stored in ~/.openclaw/credentials/whatsapp/<accountId>/creds.json.
  • Backup copy at creds.json.bak (restored on corruption).
  • Legacy compatibility: older installs stored Baileys files directly in ~/.openclaw/credentials/.
  • Logout: openclaw channels logout (or --account <id>) deletes WhatsApp auth state (but keeps shared oauth.json).
  • Logged-out socket => error instructs re-link.

Inbound flow (DM + group)

  • WhatsApp events come from messages.upsert (Baileys).
  • Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
  • Status/broadcast chats are ignored.
  • Direct chats use E.164; groups use group JID.
  • DM policy: channels.whatsapp.dmPolicy controls direct chat access (default: pairing).
    • Pairing: unknown senders get a pairing code (approve via openclaw pairing approve whatsapp <code>; codes expire after 1 hour).
    • Open: requires channels.whatsapp.allowFrom to include "*".
    • Your linked WhatsApp number is implicitly trusted, so self messages skip ⁠channels.whatsapp.dmPolicy and channels.whatsapp.allowFrom checks.
  • Pairing: unknown senders get a pairing code (approve via openclaw pairing approve whatsapp <code>; codes expire after 1 hour).
  • Open: requires channels.whatsapp.allowFrom to include "*".
  • Your linked WhatsApp number is implicitly trusted, so self messages skip ⁠channels.whatsapp.dmPolicy and channels.whatsapp.allowFrom checks.
  • Pairing: unknown senders get a pairing code (approve via openclaw pairing approve whatsapp <code>; codes expire after 1 hour).
  • Open: requires channels.whatsapp.allowFrom to include "*".
  • Your linked WhatsApp number is implicitly trusted, so self messages skip ⁠channels.whatsapp.dmPolicy and channels.whatsapp.allowFrom checks.

Personal-number mode (fallback)

  • Outbound DMs never trigger pairing replies (prevents spamming contacts).
  • Inbound unknown senders still follow channels.whatsapp.dmPolicy.
  • Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
  • Read receipts sent for non-self-chat DMs.

Read receipts

text
{
  channels: { whatsapp: { sendReadReceipts: false } },
}
text
{
  channels: {
    whatsapp: {
      accounts: {
        personal: { sendReadReceipts: false },
      },
    },
  },
}
  • Self-chat mode always skips read receipts.

WhatsApp FAQ: sending messages + pairing

  • First DM from a new sender returns a short code (message is not processed).
  • Approve with: openclaw pairing approve whatsapp <code> (list with openclaw pairing list whatsapp).
  • Codes expire after 1 hour; pending requests are capped at 3 per channel.

Message normalization (what the model sees)

  • Body is the current message body with envelope.
  • Quoted reply context is always appended:
    [Replying to +1555 id:ABC123]
    <quoted text or <media:...>>
    [/Replying]
    
  • Reply metadata also set:
    • ReplyToId = stanzaId
    • ReplyToBody = quoted body or media placeholder
    • ReplyToSender = E.164 when known
  • ReplyToId = stanzaId
  • ReplyToBody = quoted body or media placeholder
  • ReplyToSender = E.164 when known
  • Media-only inbound messages use placeholders:
    • <media:image|video|audio|document|sticker>
  • <media:image|video|audio|document|sticker>
text
[Replying to +1555 id:ABC123]
<quoted text or <media:...>>
[/Replying]
  • ReplyToId = stanzaId
  • ReplyToBody = quoted body or media placeholder
  • ReplyToSender = E.164 when known
  • <media:image|video|audio|document|sticker>

Groups

  • Groups map to agent:<agentId>:whatsapp:group:<jid> sessions.
  • Group policy: channels.whatsapp.groupPolicy = open|disabled|allowlist (default allowlist).
  • Activation modes:
    • mention (default): requires @mention or regex match.
    • always: always triggers.
  • mention (default): requires @mention or regex match.
  • always: always triggers.
  • /activation mention|always is owner-only and must be sent as a standalone message.
  • Owner = channels.whatsapp.allowFrom (or self E.164 if unset).
  • History injection (pending-only):
    • Recent unprocessed messages (default 50) inserted under: [Chat messages since your last reply - for context] (messages already in the session are not re-injected)
    • Current message under: [Current message - respond to this]
    • Sender suffix appended: [from: Name (+E164)]
  • Recent unprocessed messages (default 50) inserted under: [Chat messages since your last reply - for context] (messages already in the session are not re-injected)
  • Current message under: [Current message - respond to this]
  • Sender suffix appended: [from: Name (+E164)]
  • Group metadata cached 5 min (subject + participants).
  • mention (default): requires @mention or regex match.
  • always: always triggers.
  • Recent unprocessed messages (default 50) inserted under: [Chat messages since your last reply - for context] (messages already in the session are not re-injected)
  • Current message under: [Current message - respond to this]
  • Sender suffix appended: [from: Name (+E164)]

Reply delivery (threading)

  • WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
  • Reply tags are ignored on this channel.

Acknowledgment reactions (auto-react on receipt)

text
{
  "whatsapp": {
    "ackReaction": {
      "emoji": "👀",
      "direct": true,
      "group": "mentions"
    }
  }
}
  • emoji (string): Emoji to use for acknowledgment (e.g., ”👀”, ”✅”, ”📨”). Empty or omitted = feature disabled.
  • direct (boolean, default: true): Send reactions in direct/DM chats.
  • group (string, default: "mentions"): Group chat behavior:
    • "always": React to all group messages (even without @mention)
    • "mentions": React only when bot is @mentioned
    • "never": Never react in groups
  • "always": React to all group messages (even without @mention)
  • "mentions": React only when bot is @mentioned
  • "never": Never react in groups
  • "always": React to all group messages (even without @mention)
  • "mentions": React only when bot is @mentioned
  • "never": Never react in groups
text
{
  "whatsapp": {
    "accounts": {
      "work": {
        "ackReaction": {
          "emoji": "✅",
          "direct": false,
          "group": "always"
        }
      }
    }
  }
}
  • Reactions are sent immediately upon message receipt, before typing indicators or bot replies.
  • In groups with requireMention: false (activation: always), group: "mentions" will react to all messages (not just @mentions).
  • Fire-and-forget: reaction failures are logged but don’t prevent the bot from replying.
  • Participant JID is automatically included for group reactions.
  • WhatsApp ignores messages.ackReaction; use channels.whatsapp.ackReaction instead.

Agent tool (reactions)

  • Tool: whatsapp with react action (chatJid, messageId, emoji, optional remove).
  • Optional: participant (group sender), fromMe (reacting to your own message), accountId (multi-account).
  • Reaction removal semantics: see /tools/reactions.
  • Tool gating: channels.whatsapp.actions.reactions (default: enabled).

Limits

  • Outbound text is chunked to channels.whatsapp.textChunkLimit (default 4000).
  • Optional newline chunking: set channels.whatsapp.chunkMode="newline" to split on blank lines (paragraph boundaries) before length chunking.
  • Inbound media saves are capped by channels.whatsapp.mediaMaxMb (default 50 MB).
  • Outbound media items are capped by agents.defaults.mediaMaxMb (default 5 MB).

Outbound send (text + media)

  • Uses active web listener; error if gateway not running.
  • Text chunking: 4k max per message (configurable via channels.whatsapp.textChunkLimit, optional channels.whatsapp.chunkMode).
  • Media:
    • Image/video/audio/document supported.
    • Audio sent as PTT; audio/ogg => audio/ogg; codecs=opus.
    • Caption only on first media item.
    • Media fetch supports HTTP(S) and local paths.
    • Animated GIFs: WhatsApp expects MP4 with gifPlayback: true for inline looping.
      • CLI: openclaw message send --media <mp4> --gif-playback
      • Gateway: send params include gifPlayback: true
  • Image/video/audio/document supported.
  • Audio sent as PTT; audio/ogg => audio/ogg; codecs=opus.
  • Caption only on first media item.
  • Media fetch supports HTTP(S) and local paths.
  • Animated GIFs: WhatsApp expects MP4 with gifPlayback: true for inline looping.
    • CLI: openclaw message send --media <mp4> --gif-playback
    • Gateway: send params include gifPlayback: true
  • CLI: openclaw message send --media <mp4> --gif-playback
  • Gateway: send params include gifPlayback: true
  • Image/video/audio/document supported.
  • Audio sent as PTT; audio/ogg => audio/ogg; codecs=opus.
  • Caption only on first media item.
  • Media fetch supports HTTP(S) and local paths.
  • Animated GIFs: WhatsApp expects MP4 with gifPlayback: true for inline looping.
    • CLI: openclaw message send --media <mp4> --gif-playback
    • Gateway: send params include gifPlayback: true
  • CLI: openclaw message send --media <mp4> --gif-playback
  • Gateway: send params include gifPlayback: true
  • CLI: openclaw message send --media <mp4> --gif-playback
  • Gateway: send params include gifPlayback: true

Voice notes (PTT audio)

  • Best results: OGG/Opus. OpenClaw rewrites audio/ogg to audio/ogg; codecs=opus.
  • [[audio_as_voice]] is ignored for WhatsApp (audio already ships as voice note).

Media limits + optimization

  • Default outbound cap: 5 MB (per media item).
  • Override: agents.defaults.mediaMaxMb.
  • Images are auto-optimized to JPEG under cap (resize + quality sweep).
  • Oversize media => error; media reply falls back to text warning.

Heartbeats

  • Gateway heartbeat logs connection health (web.heartbeatSeconds, default 60s).
  • Agent heartbeat can be configured per agent (agents.list[].heartbeat) or globally via agents.defaults.heartbeat (fallback when no per-agent entries are set).
    • Uses the configured heartbeat prompt (default: Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.) + HEARTBEAT_OK skip behavior.
    • Delivery defaults to the last used channel (or configured target).
  • Uses the configured heartbeat prompt (default: Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.) + HEARTBEAT_OK skip behavior.
  • Delivery defaults to the last used channel (or configured target).
  • Uses the configured heartbeat prompt (default: Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.) + HEARTBEAT_OK skip behavior.
  • Delivery defaults to the last used channel (or configured target).

Reconnect behavior

  • Backoff policy: web.reconnect:
    • initialMs, maxMs, factor, jitter, maxAttempts.
  • initialMs, maxMs, factor, jitter, maxAttempts.
  • If maxAttempts reached, web monitoring stops (degraded).
  • Logged-out => stop and require re-link.
  • initialMs, maxMs, factor, jitter, maxAttempts.

Config quick map

  • channels.whatsapp.dmPolicy (DM policy: pairing/allowlist/open/disabled).
  • channels.whatsapp.selfChatMode (same-phone setup; bot uses your personal WhatsApp number).
  • channels.whatsapp.allowFrom (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames).
  • channels.whatsapp.mediaMaxMb (inbound media save cap).
  • channels.whatsapp.ackReaction (auto-reaction on message receipt: {emoji, direct, group}).
  • channels.whatsapp.accounts.<accountId>.* (per-account settings + optional authDir).
  • channels.whatsapp.accounts.<accountId>.mediaMaxMb (per-account inbound media cap).
  • channels.whatsapp.accounts.<accountId>.ackReaction (per-account ack reaction override).
  • channels.whatsapp.groupAllowFrom (group sender allowlist).
  • channels.whatsapp.groupPolicy (group policy).
  • channels.whatsapp.historyLimit / channels.whatsapp.accounts.<accountId>.historyLimit (group history context; 0 disables).
  • channels.whatsapp.dmHistoryLimit (DM history limit in user turns). Per-user overrides: channels.whatsapp.dms["<phone>"].historyLimit.
  • channels.whatsapp.groups (group allowlist + mention gating defaults; use "*" to allow all)
  • channels.whatsapp.actions.reactions (gate WhatsApp tool reactions).
  • agents.list[].groupChat.mentionPatterns (or messages.groupChat.mentionPatterns)
  • messages.groupChat.historyLimit
  • channels.whatsapp.messagePrefix (inbound prefix; per-account: channels.whatsapp.accounts.<accountId>.messagePrefix; deprecated: messages.messagePrefix)
  • messages.responsePrefix (outbound prefix)
  • agents.defaults.mediaMaxMb
  • agents.defaults.heartbeat.every
  • agents.defaults.heartbeat.model (optional override)
  • agents.defaults.heartbeat.target
  • agents.defaults.heartbeat.to
  • agents.defaults.heartbeat.session
  • agents.list[].heartbeat.* (per-agent overrides)
  • session.* (scope, idle, store, mainKey)
  • web.enabled (disable channel startup when false)
  • web.heartbeatSeconds
  • web.reconnect.*

Logs + troubleshooting

  • Subsystems: whatsapp/inbound, whatsapp/outbound, web-heartbeat, web-reconnect.
  • Log file: /tmp/openclaw/openclaw-YYYY-MM-DD.log (configurable).
  • Troubleshooting guide: Gateway troubleshooting.

Troubleshooting (quick)

  • Symptom: channels status shows linked: false or warns “Not linked”.
  • Fix: run openclaw channels login on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
  • Symptom: channels status shows running, disconnected or warns “Linked but disconnected”.
  • Fix: openclaw doctor (or restart the gateway). If it persists, relink via channels login and inspect openclaw logs --follow.
  • Bun is not recommended. WhatsApp (Baileys) and Telegram are unreliable on Bun. Run the gateway with Node. (See Getting Started runtime note.)