Browse docs
Docs / channels / whatsapp
WhatsApp (web channel)
Quick setup (beginner)
- Use a separate phone number if possible (recommended).
- Configure WhatsApp in
~/.openclaw/openclaw.json. - Run
openclaw channels loginto scan the QR code (Linked Devices). - 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)
Dedicated number (recommended)
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
--accountis omitted):defaultif 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 sharedoauth.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.dmPolicycontrols 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.allowFromto include"*". - Your linked WhatsApp number is implicitly trusted, so self messages skip
channels.whatsapp.dmPolicyandchannels.whatsapp.allowFromchecks.
- Pairing: unknown senders get a pairing code (approve via
- Pairing: unknown senders get a pairing code (approve via
openclaw pairing approve whatsapp <code>; codes expire after 1 hour). - Open: requires
channels.whatsapp.allowFromto include"*". - Your linked WhatsApp number is implicitly trusted, so self messages skip
channels.whatsapp.dmPolicyandchannels.whatsapp.allowFromchecks.
- Pairing: unknown senders get a pairing code (approve via
openclaw pairing approve whatsapp <code>; codes expire after 1 hour). - Open: requires
channels.whatsapp.allowFromto include"*". - Your linked WhatsApp number is implicitly trusted, so self messages skip
channels.whatsapp.dmPolicyandchannels.whatsapp.allowFromchecks.
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 withopenclaw pairing list whatsapp). - Codes expire after 1 hour; pending requests are capped at 3 per channel.
Message normalization (what the model sees)
Bodyis the current message body with envelope.- Quoted reply context is always appended:
- Reply metadata also set:
ReplyToId= stanzaIdReplyToBody= quoted body or media placeholderReplyToSender= E.164 when known
ReplyToId= stanzaIdReplyToBody= quoted body or media placeholderReplyToSender= 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= stanzaIdReplyToBody= quoted body or media placeholderReplyToSender= 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(defaultallowlist). - Activation modes:
mention(default): requires @mention or regex match.always: always triggers.
mention(default): requires @mention or regex match.always: always triggers./activation mention|alwaysis 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:
- 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; usechannels.whatsapp.ackReactioninstead.
Agent tool (reactions)
- Tool:
whatsappwithreactaction (chatJid,messageId,emoji, optionalremove). - 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, optionalchannels.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: truefor inline looping.- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: true
- CLI:
- 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: truefor inline looping.- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: true
- CLI:
- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: 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: truefor inline looping.- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: true
- CLI:
- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: true
- CLI:
openclaw message send --media <mp4> --gif-playback - Gateway:
sendparams includegifPlayback: true
Voice notes (PTT audio)
- Best results: OGG/Opus. OpenClaw rewrites
audio/oggtoaudio/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 viaagents.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_OKskip behavior. - Delivery defaults to the last used channel (or configured target).
- Uses the configured heartbeat prompt (default:
- 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_OKskip 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_OKskip 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 + optionalauthDir).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;0disables).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(ormessages.groupChat.mentionPatterns)messages.groupChat.historyLimitchannels.whatsapp.messagePrefix(inbound prefix; per-account:channels.whatsapp.accounts.<accountId>.messagePrefix; deprecated:messages.messagePrefix)messages.responsePrefix(outbound prefix)agents.defaults.mediaMaxMbagents.defaults.heartbeat.everyagents.defaults.heartbeat.model(optional override)agents.defaults.heartbeat.targetagents.defaults.heartbeat.toagents.defaults.heartbeat.sessionagents.list[].heartbeat.*(per-agent overrides)session.*(scope, idle, store, mainKey)web.enabled(disable channel startup when false)web.heartbeatSecondsweb.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 statusshowslinked: falseor warns “Not linked”. - Fix: run
openclaw channels loginon the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
- Symptom:
channels statusshowsrunning, disconnectedor warns “Linked but disconnected”. - Fix:
openclaw doctor(or restart the gateway). If it persists, relink viachannels loginand inspectopenclaw logs --follow.
- Bun is not recommended. WhatsApp (Baileys) and Telegram are unreliable on Bun. Run the gateway with Node. (See Getting Started runtime note.)