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

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

Diagram

Web Mode (Experimental)

Cloud API (Production)

Webhook

WebSocket

Meta Cloud API

🤖 Bot

📱 Scan QR

WhatsApp Servers

🤖 Bot

🧠 AI Agent

Quick Start

Web Mode (Easiest)

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

  1. Install
    bash
    pip install 'praisonai[bot-whatsapp-web]'
  2. Start
    bash
    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.

Cloud API (Production)

Uses the official Meta WhatsApp Business API.

  1. Get Credentials
    1. Go to Meta for Developers
    2. Create an app → add the WhatsApp product
    3. Copy your Phone Number ID and Access Token
  2. Set Environment Variables
    bash
    export WHATSAPP_ACCESS_TOKEN="EAAx..."
    export WHATSAPP_PHONE_NUMBER_ID="123456789"
    export WHATSAPP_VERIFY_TOKEN="my-secret"
  3. Start
    bash
    praisonai bot whatsapp

    This starts a webhook server on port 8080. Point your Meta webhook URL to https://your-domain.com/webhook.

Python SDK

python
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

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.

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

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

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])

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.

Diagram
UserCloud APIWhatsAppPresentationRendererWhatsAppBotAgentUserCloud APIWhatsAppPresentationRendererWhatsAppBotAgentrender_presentation(target, presentation)render(presentation)adapt_presentation(whatsapp limits)interactive payload (button / list)POST interactive messageTappable buttons / listTapCommand callback

Approval prompt (reply buttons)

python
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)

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

Diagram

Yes

No

Yes

No

Yes

No

Yes

No

📩 Message

🪪 Canonicalize\nLID → phone

Older than\nconnect time?

🗑️ Drop

Self-chat?\n(you → your number)

✅ Process

Outgoing to\nsomeone else?

❌ Block

In allowlist?

❌ Ignore

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

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>@lid or <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)

bash
praisonai bot whatsapp --mode web

Only responds when you message your own number.

Specific Numbers

bash
# Comma-separated
praisonai bot whatsapp --mode web --respond-to 1234567890,9876543210

# Or repeated flags
praisonai bot whatsapp --mode web --respond-to 1234567890 --respond-to 9876543210

Specific Groups

bash
# 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.us

Everyone

bash
praisonai bot whatsapp --mode web --respond-to-all

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

Python SDK Filtering

python
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

yaml
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"
bash
praisonai bot start --config bot.yaml

Built-in Commands

CommandDescription
/helpShow available commands
/statusBot info, model, uptime
/newReset conversation session

Register custom commands:

python
@bot.on_command("ping")
async def ping(msg):
    return "Pong!"

CLI Options

bash
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

Diagram

Message Processing

WhatsApp Bot Runtime

Web mode

Cloud mode

Pass

Response

praisonai bot whatsapp

WhatsAppBot

WhatsAppWebAdapter

Webhook Server

neonize Client

WhatsApp Servers

Message Filter

Session Manager

AI Agent

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 FilterIdentity canonicalize (LID → phone) → stale 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).

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

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:

bash
pip install --upgrade praisonai
Bot 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 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.

SessionCipher error: old counter
[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.

WebSocket close error on shutdown
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.

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

Fixed in latest version. The bot now properly shuts down background threads on exit. Update your installation.

QR code not showing
  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:
bash
rm -rf ~/.praisonai/whatsapp/
Session expired

WhatsApp Web sessions expire if your phone is offline for 14+ days. Delete and re-scan:

bash
rm -rf ~/.praisonai/whatsapp/
praisonai bot whatsapp --mode web

Best 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