WhatsApp Bot
Connect your AI agent to WhatsApp — Cloud API or Web mode
Connect your AI agent to WhatsApp with a single command. Choose between the official Cloud API (production) or Web mode (scan a QR code, no tokens needed).
from praisonaiagents import Agent
from praisonai.bots import WhatsAppBot
agent = Agent(name="assistant", instructions="Be helpful on WhatsApp.", llm="gpt-4o-mini")
bot = WhatsAppBot(mode="web", agent=agent)
import asyncio
asyncio.run(bot.start())The user messages on WhatsApp; the bot forwards the chat to the agent and replies in the thread.
Quick Start
Web Mode (Easiest)
No tokens, no developer account — just scan a QR code.
- Install
bash pip install 'praisonai[bot-whatsapp-web]' - Start
bash praisonai bot whatsapp --mode webA QR code appears in your terminal. Open WhatsApp → Linked Devices → scan it.
- Chat
Open your own contact (message yourself) and send a message. The bot replies.
Cloud API (Production)
Uses the official Meta WhatsApp Business API.
- Get Credentials
- Go to Meta for Developers
- Create an app → add the WhatsApp product
- Copy your Phone Number ID and Access Token
- Set Environment Variables
bash export WHATSAPP_ACCESS_TOKEN="EAAx..." export WHATSAPP_PHONE_NUMBER_ID="123456789" export WHATSAPP_VERIFY_TOKEN="my-secret" - Start
bash praisonai bot whatsappThis starts a webhook server on port 8080. Point your Meta webhook URL to
https://your-domain.com/webhook.
Python SDK
from praisonaiagents import Agent
from praisonai.bots import WhatsAppBot
agent = Agent(
name="assistant",
instructions="You are a helpful AI assistant",
llm="gpt-4o-mini"
)
# Web mode — no tokens needed
bot = WhatsAppBot(mode="web", agent=agent)
import asyncio
asyncio.run(bot.start())How It Works
Cloud API vs Web Mode
| Feature | Cloud API | Web Mode |
|---|---|---|
| Setup | Meta developer account + tokens | QR code scan |
| Stability | Official, stable | Reverse-engineered, may break |
| Webhooks | Required (public HTTPS) | Not needed |
| Groups | DMs only | DMs + groups |
| Risk | None | Account may be banned |
| Best for | Production | Development, personal use |
Inbound Media → Vision
Photos and documents sent by users are downloaded via the WhatsApp Graph API, validated, and forwarded to your agent’s vision capability — no placeholder text, no extra code.
sequenceDiagram
participant U as 👤 User
participant WA as 📲 WhatsApp
participant Bot as 🤖 WhatsApp Bot
participant M as 🛡️ Media Validator
participant A as 🧠 Agent Vision
U->>WA: Send photo + caption
WA->>Bot: Webhook (image id + caption)
Bot->>WA: Download media via Graph API
WA-->>Bot: Raw bytes
Bot->>M: cache_inbound_media()
Note over M: Size cap check (≤ 20 MiB)<br/>Magic-byte content-type check<br/>SSRF-safe URL fetch
M-->>Bot: Validated local path
Bot->>A: chat(prompt=caption, attachments=[path])
A-->>Bot: Vision response
Bot-->>U: 📨 Reply with image description
classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
classDef platform fill:#189AB4,stroke:#7C90A0,color:#fff
classDef security fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef agent fill:#10B981,stroke:#7C90A0,color:#fff- Vision agent — works out of the box
Any vision-capable model receives photos automatically. No configuration needed.
python from praisonaiagents import Agent from praisonai.bots import WhatsAppBot agent = Agent( name="vision-assistant", instructions="Describe and answer questions about images.", llm="gpt-4o", ) bot = WhatsAppBot(agent=agent) import asyncio asyncio.run(bot.start())Send a photo to your WhatsApp business number. The agent sees the image and replies.
- Limit or disable inbound media
Control media size or disable it entirely with
max_inbound_media_bytes.python from praisonaiagents import Agent from praisonai.bots import WhatsAppBot from praisonaiagents import BotConfig agent = Agent(name="assistant", instructions="Be helpful.", llm="gpt-4o") bot = WhatsAppBot( agent=agent, config=BotConfig( max_inbound_media_bytes=5 * 1024 * 1024, # 5 MiB cap # max_inbound_media_bytes=0 # set 0 to disable entirely ), ) import asyncio asyncio.run(bot.start())
How the security pipeline works
| Step | What happens |
|---|---|
| Download | Bot fetches media from WhatsApp Graph API using the media id |
| Size cap | File rejected if it exceeds max_inbound_media_bytes (default 20 MiB) |
| Magic-byte check | Content type verified against actual file bytes — not just the declared MIME |
| SSRF guard | URL fetch rejects non-http(s) schemes and private/loopback/link-local hosts |
| Forward | Validated path passed to agent.chat(attachments=[path]) |
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
max_inbound_media_bytes | int | 20971520 (20 MiB) | Maximum size for inbound media. Set 0 to disable inbound media entirely. |
Interactive Messages
WhatsApp bots render MessagePresentation natively — approval buttons, quick replies, and select menus become tappable Cloud API interactive messages, with no code change to existing agents.
Approval prompt (reply buttons)
import asyncio
from praisonaiagents.bots import MessagePresentation
from praisonai_bot.bots import WhatsAppBot
bot = WhatsAppBot(
token="YOUR_TOKEN",
phone_number_id="YOUR_PHONE_ID",
)
presentation = MessagePresentation.approval(
prompt="Allow file delete?",
approval_id="appr-1",
)
async def main():
# ≤3 non-URL buttons → native reply-button message
await bot.render_presentation("15551234567", presentation)
asyncio.run(main())Menu / select (list message)
import asyncio
from praisonaiagents.bots import MessagePresentation, PresentationBlock, SelectOption
from praisonai_bot.bots import WhatsAppBot
bot = WhatsAppBot(
token="YOUR_TOKEN",
phone_number_id="YOUR_PHONE_ID",
)
presentation = MessagePresentation(blocks=[
PresentationBlock.make_text("Pick an environment:"),
PresentationBlock.make_select(
options=[
SelectOption(label="Staging", value="staging"),
SelectOption(label="Production", value="prod"),
],
placeholder="Choose",
action_id="env",
),
])
async def main():
# select block → native list message (up to 10 rows)
await bot.render_presentation("15551234567", presentation)
asyncio.run(main())How WhatsApp adapts
| Presentation content | Native shape |
|---|---|
| ≤3 non-URL buttons | Reply buttons (interactive.type == "button") |
>3 buttons or a select block | List message (interactive.type == "list", up to 10 rows) |
| URL buttons | Inlined into the body as Label: URL text links |
| Text only | Plain text send |
See Bot Presentations for the portable model shared across Telegram, Slack, Discord, and WhatsApp.
Message Filtering
By default, the bot responds only to self-chat — when you message your own number. This prevents it from replying to every conversation.
Identity canonicalization runs first, so every later check — self-chat, outgoing guard, and allowlist — sees one stable phone-form identity regardless of whether the message arrived as a <lid>@lid or <phone>@s.whatsapp.net JID. See Identity: LID vs phone JID.
Four Layers of Protection
Messages older than when the bot connected are dropped. Prevents replaying old conversations on reconnect.
Self-Chat CheckOnly messages where sender = chat JID pass.
Outgoing GuardYour messages sent to other people’s chats are always blocked — even if they’re in the allowlist.
AllowlistsOptionally allow specific phone numbers or groups.
Identity: LID vs phone JID
In Web mode, WhatsApp may address the same person with two interchangeable JIDs — the privacy LID (<lid>@lid) and the phone JID (<phone>@s.whatsapp.net) — and the web bridge can surface either form for the same person across messages.
The adapter reconciles both to the phone form before any allowlist, session, memory, or pairing key is derived. It learns the mapping from whatsmeow’s SenderAlt alternate-JID field, so:
- Allowlists match either form. A phone-number entry authorises the same person whether they arrive as
<lid>@lidor<phone>@s.whatsapp.net. - One session and memory bucket. LID-form and phone-form turns from the same person resolve to a single canonical id — no split history.
- Stable pairing. Pairing state carries across both forms, so an already-paired user is never asked to re-pair.
- Self-chat stays self-chat. Same-session self-chat detection compares the canonicalized chat form, so an LID-addressed self-chat is never mistaken for an outgoing message and dropped.
No configuration is required. An unknown address is returned unchanged (fail-open) — behaviour is never worse than passing the raw JID through.
Expand Who Can Message the Bot
Self-Only (Default)
praisonai bot whatsapp --mode webOnly responds when you message your own number.
Specific Numbers
# Comma-separated
praisonai bot whatsapp --mode web --respond-to 1234567890,9876543210
# Or repeated flags
praisonai bot whatsapp --mode web --respond-to 1234567890 --respond-to 9876543210Specific Groups
# Comma-separated
praisonai bot whatsapp --mode web --respond-to-groups 120363123456@g.us,999888777@g.us
# Or repeated flags
praisonai bot whatsapp --mode web --respond-to-groups 120363123456@g.us --respond-to-groups 999888777@g.usEveryone
praisonai bot whatsapp --mode web --respond-to-allFiltering Matrix
| Scenario | Default | --respond-to 123 | --respond-to-groups g@g.us | --respond-to-all |
|---|---|---|---|---|
| Self-chat (your own number) | ✅ | ✅ | ✅ | ✅ |
| Your msg in someone else’s chat | ❌ | ❌ | ❌ | ✅ |
| DM from 123 | ❌ | ✅ | ❌ | ✅ |
| DM from 999 | ❌ | ❌ | ❌ | ✅ |
| Group g@g.us | ❌ | ❌ | ✅ | ✅ |
| Other group | ❌ | ❌ | ❌ | ✅ |
| Old/offline messages | ❌ | ❌ | ❌ | ❌ |
Python SDK Filtering
from praisonai.bots import WhatsAppBot
bot = WhatsAppBot(
mode="web",
agent=agent,
allowed_numbers=["1234567890", "9876543210"],
allowed_groups=["120363123456@g.us"],
respond_to_all=False, # default
)YAML Config
platform: whatsapp
mode: web
respond_to:
- "1234567890"
respond_to_groups:
- "120363123456@g.us"
respond_to_all: false
agent:
name: "My Assistant"
instructions: "You are a helpful AI assistant."
llm: "gpt-4o-mini"praisonai bot start --config bot.yamlBuilt-in Commands
| Command | Description |
|---|---|
/help | Show available commands |
/status | Bot info, model, uptime |
/new | Reset conversation session |
Register custom commands:
@bot.on_command("ping")
async def ping(msg):
return "Pong!"CLI Options
praisonai bot whatsapp --mode web [OPTIONS]| Flag | Description | Example |
|---|---|---|
--mode | cloud (default) or web | --mode web |
--respond-to | Allowed phone numbers (comma-separated or repeated) | --respond-to 123,456 or --respond-to 123 --respond-to 456 |
--respond-to-groups | Allowed group JIDs (comma-separated or repeated) | --respond-to-groups grp@g.us |
--respond-to-all | Respond to everyone | --respond-to-all |
--creds-dir | Credentials directory | --creds-dir ~/.myapp/wa |
--agent | Agent YAML config | --agent agents.yaml |
--memory | Enable conversation memory | --memory |
--web | Enable web search tool | --web |
--thinking | Extended thinking mode | --thinking high |
--auto-approve | Auto-approve tool calls | --auto-approve |
Architecture
Key Components
| Component | Role |
|---|---|
| WhatsAppBot | Main bot class — handles lifecycle, filtering, routing |
| WhatsAppWebAdapter | Bridges neonize (Go) events to Python asyncio |
| Session Manager | Per-user conversation sessions with expiry |
| Message Filter | Identity canonicalize (LID → phone) → stale guard → self-chat check → allowlist check |
| AI Agent | Your configured agent processes the message and responds |
Long Agent Turns
A long agent turn on WhatsApp — deep research, big refactor, a slow model — stays BUSY for as long as it is making any progress, and is never restarted mid-stream.
The gateway watches each channel’s health and only restarts a channel when it is genuinely wedged. Progress comes from inbound transport activity (someone messaged the bot) and in-run progress (the agent emitted a streamed token, called a tool, or fired an emitter event). As long as either signal advanced within stuck_after (default 900s), the channel stays BUSY and will not be killed. Only when no signal arrives for stuck_after seconds does the channel escalate to STUCK (recoverable, will be restarted).
sequenceDiagram
participant User as WhatsApp User
participant WA as WhatsApp Adapter
participant Agent
participant Health as Channel Health
User->>WA: Run a deep research task
WA->>Agent: start run (active_runs=1)
Agent-->>WA: streamed token
WA->>Health: note_run_progress()
Agent-->>WA: tool call
WA->>Health: note_run_progress()
Health-->>WA: BUSY (within stuck_after)
Agent-->>WA: final answer
WA-->>User: reply
classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
class Agent agent
class User,WA tool
class Health decision| Scenario on WhatsApp | Health state | Outcome |
|---|---|---|
| Active inbound traffic + active run | BUSY | Channel kept alive |
Active run, no inbound, streaming tokens or tool calls within stuck_after | BUSY | Channel kept alive |
Active run, no signal of any kind for > stuck_after | STUCK | Channel restarted (genuine hang) |
| No active runs, idle | IDLE | Channel kept alive |
Troubleshooting
Bot responds to old messages on startup
Fixed in latest version. The stale-message guard drops any message older than when the bot connected. If you still see this, update to the latest version:
pip install --upgrade praisonaiBot responds to all messages, not just self-chat
Fixed in latest version. The default filter now checks that both the sender AND chat JID match (true self-chat), not just IsFromMe. Update your installation.
Decryption warnings in the logs
WARNING Ignoring message ... failed to decrypt prekey message
WARNING Ignoring message ... failed to decrypt group messageThese are normal and harmless. They come from the WhatsApp protocol layer (whatsmeow) when old session keys can’t decrypt certain messages. The bot automatically suppresses most of these. They don’t affect functionality.
SessionCipher error: old counter
[ERROR] SessionCipher.go:310 ▶ Unable to get or create message keysThis is an upstream protocol error from the Signal encryption layer. It means a message had expired keys. This is not a bug in PraisonAI — it’s a normal part of the WhatsApp protocol. The message is simply skipped.
WebSocket close error on shutdown
WARNING Error sending close to websocketThis appears when Ctrl+C is pressed. It’s harmless — the bot is disconnecting from WhatsApp servers. The latest version suppresses this warning.
shell_tools error: /home/user not found
ERROR Error executing command: No such file or directory: '/home/user'This happens when the AI agent tries to run a command in /home/user (a Linux path) on macOS. Fixed in latest version — the tool now automatically falls back to your home directory.
Threading error on Ctrl+C
Exception ignored in: <module 'threading' ...>
KeyboardInterruptFixed in latest version. The bot now properly shuts down background threads on exit. Update your installation.
QR code not showing
- Use a modern terminal (iTerm2, Windows Terminal)
- Ensure
segnois installed:pip install 'praisonai[bot-whatsapp-web]' - If a saved session exists, no QR is needed. Delete to re-link:
rm -rf ~/.praisonai/whatsapp/Session expired
WhatsApp Web sessions expire if your phone is offline for 14+ days. Delete and re-scan:
rm -rf ~/.praisonai/whatsapp/
praisonai bot whatsapp --mode webBest Practices
Use Cloud API for production, Web mode for prototyping
Web mode (mode="web") needs only a QR-code scan and no tokens — perfect for demos. For anything user-facing, switch to the official Cloud API (mode="cloud"), which is stable, supports webhooks, and won’t break when the linked phone goes offline.
Keep tokens in the environment
Never hard-code the Cloud API token or phone-number ID in source. Load them from environment variables so the same bot code runs across staging and production without leaking credentials into version control.
Validate inbound media before the agent sees it
WhatsApp accepts images, audio, and documents. Rely on the built-in inbound-media security pipeline (SSRF guard, type checks) and message filtering rather than passing raw attachments straight to the model — malicious payloads are stopped at the edge.
Acknowledge long turns
Agent replies that take time can look like the bot has stalled. Send a quick status message for long-running turns so users know work is in progress, keeping the WhatsApp thread responsive.
Standalone scheduled delivery (no gateway)
You do not need a running WhatsApp Bot process to deliver a scheduled brief to WhatsApp. From OS cron / CI / a scale-to-zero deployment, praisonai schedule tick will POST directly to the WhatsApp Cloud API using the same env the live bot reads (WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, and either an explicit deliver: whatsapp:<recipient> target or WHATSAPP_HOME_CHANNEL). Sends are retried up to 4 times with exponential backoff on transient failures. See Out-of-process delivery.
Related
Full reference for inbound media: security pipeline, SSRF guard, and patterns
Messaging BotsAll supported platforms: Telegram, Discord, Slack, WhatsApp
WhatsApp MCPSend WhatsApp messages from agents using MCP tools
Bot CommandsCustom command registration and handling
Approval ProtocolHuman-in-the-loop tool approval for bots