Skip to main content
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

No tokens, no developer account — just scan a QR code.
1

Install

pip install 'praisonai[bot-whatsapp-web]'
2

Start

praisonai bot whatsapp --mode web
A QR code appears in your terminal. Open WhatsApp → Linked Devices → scan it.
3

Chat

Open your own contact (message yourself) and send a message. The bot replies.
Experimental — Web mode uses a reverse-engineered protocol. Your number may be banned by WhatsApp. Use Cloud API for production.

How It Works

Cloud API vs Web Mode

FeatureCloud APIWeb Mode
SetupMeta developer account + tokensQR code scan
StabilityOfficial, stableReverse-engineered, may break
WebhooksRequired (public HTTPS)Not needed
GroupsDMs onlyDMs + groups
RiskNoneAccount may be banned
Best forProductionDevelopment, 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.
1

Vision agent — works out of the box

Any vision-capable model receives photos automatically. No configuration needed.
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.
2

Limit or disable inbound media

Control media size or disable it entirely with max_inbound_media_bytes.
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

StepWhat happens
DownloadBot fetches media from WhatsApp Graph API using the media id
Size capFile rejected if it exceeds max_inbound_media_bytes (default 20 MiB)
Magic-byte checkContent type verified against actual file bytes — not just the declared MIME
SSRF guardURL fetch rejects non-http(s) schemes and private/loopback/link-local hosts
ForwardValidated path passed to agent.chat(attachments=[path])
What if my agent has no vision capability? The adapter checks whether agent.chat() accepts attachments. If it doesn’t, the attachment is silently skipped and only the caption text is forwarded. Your existing text-only agents keep working with zero changes.

Configuration

OptionTypeDefaultDescription
max_inbound_media_bytesint20971520 (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.
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())

How WhatsApp adapts

Presentation contentNative shape
≤3 non-URL buttonsReply buttons (interactive.type == "button")
>3 buttons or a select blockList message (interactive.type == "list", up to 10 rows)
URL buttonsInlined into the body as Label: URL text links
Text onlyPlain 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.

Four Layers of Protection

Stale Message Guard

Messages older than when the bot connected are dropped. Prevents replaying old conversations on reconnect.

Self-Chat Check

Only messages where sender = chat JID pass.

Outgoing Guard

Your messages sent to other people’s chats are always blocked — even if they’re in the allowlist.

Allowlists

Optionally allow specific phone numbers or groups.

Expand Who Can Message the Bot

praisonai bot whatsapp --mode web
Only responds when you message your own number.

Filtering Matrix

ScenarioDefault--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
Phone numbers are normalized automatically — +1-234-567-890 and 1234567890 match the same number.

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.yaml

Built-in Commands

CommandDescription
/helpShow available commands
/statusBot info, model, uptime
/newReset conversation session
Register custom commands:
@bot.on_command("ping")
async def ping(msg):
    return "Pong!"

CLI Options

praisonai bot whatsapp --mode web [OPTIONS]
FlagDescriptionExample
--modecloud (default) or web--mode web
--respond-toAllowed phone numbers (comma-separated or repeated)--respond-to 123,456 or --respond-to 123 --respond-to 456
--respond-to-groupsAllowed group JIDs (comma-separated or repeated)--respond-to-groups grp@g.us
--respond-to-allRespond to everyone--respond-to-all
--creds-dirCredentials directory--creds-dir ~/.myapp/wa
--agentAgent YAML config--agent agents.yaml
--memoryEnable conversation memory--memory
--webEnable web search tool--web
--thinkingExtended thinking mode--thinking high
--auto-approveAuto-approve tool calls--auto-approve

Architecture

Cloud-mode webhook verification: inbound Meta webhooks are HMAC-verified via the shared helper. Set WHATSAPP_APP_SECRET (see Messaging Bots). Details: Webhook Verification.

Key Components

ComponentRole
WhatsAppBotMain bot class — handles lifecycle, filtering, routing
WhatsAppWebAdapterBridges neonize (Go) events to Python asyncio
Session ManagerPer-user conversation sessions with expiry
Message FilterStale guard → self-chat check → allowlist check
AI AgentYour 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).
Scenario on WhatsAppHealth stateOutcome
Active inbound traffic + active runBUSYChannel kept alive
Active run, no inbound, streaming tokens or tool calls within stuck_afterBUSYChannel kept alive
Active run, no signal of any kind for > stuck_afterSTUCKChannel restarted (genuine hang)
No active runs, idleIDLEChannel kept alive
The same mechanism applies to every channel adapter, not just WhatsApp. For the full evaluator behaviour, configuration knobs (stuck_after, busy_after), and the HealthResult.last_run_progress field, see Channel Supervision.

Troubleshooting

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 praisonai
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.
WARNING Ignoring message ... failed to decrypt prekey message
WARNING Ignoring message ... failed to decrypt group message
These 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.
[ERROR] SessionCipher.go:310 ▶ Unable to get or create message keys
This 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.
WARNING Error sending close to websocket
This appears when Ctrl+C is pressed. It’s harmless — the bot is disconnecting from WhatsApp servers. The latest version suppresses this warning.
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.
Exception ignored in: <module 'threading' ...>
KeyboardInterrupt
Fixed in latest version. The bot now properly shuts down background threads on exit. Update your installation.
  1. Use a modern terminal (iTerm2, Windows Terminal)
  2. Ensure segno is installed: pip install 'praisonai[bot-whatsapp-web]'
  3. If a saved session exists, no QR is needed. Delete to re-link:
rm -rf ~/.praisonai/whatsapp/
WhatsApp Web sessions expire if your phone is offline for 14+ days. Delete and re-scan:
rm -rf ~/.praisonai/whatsapp/
praisonai bot whatsapp --mode web

Best Practices

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.
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.
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.
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.

Inbound Media

Full reference for inbound media: security pipeline, SSRF guard, and patterns

Messaging Bots

All supported platforms: Telegram, Discord, Slack, WhatsApp

WhatsApp MCP

Send WhatsApp messages from agents using MCP tools

Bot Commands

Custom command registration and handling

Approval Protocol

Human-in-the-loop tool approval for bots