> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Cloud API (Production)"
        CA[Meta Cloud API] <-->|Webhook| Bot1[🤖 Bot]
    end
    
    subgraph "Web Mode (Experimental)"
        QR[📱 Scan QR] --> WA[WhatsApp Servers]
        WA <-->|WebSocket| Bot2[🤖 Bot]
    end
    
    Bot1 --> Agent[🧠 AI Agent]
    Bot2 --> Agent
    
    classDef api fill:#189AB4,color:#fff
    classDef bot fill:#8B0000,color:#fff
    classDef agent fill:#8B0000,color:#fff
    
    class CA,WA,QR api
    class Bot1,Bot2 bot
    class Agent agent
```

## Quick Start

> [tab] Web Mode (Easiest)
>
> No tokens, no developer account — just scan a QR code.
>
> **1. Install**
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> pip install 'praisonai[bot-whatsapp-web]'
> ```
>
> **2. Start**
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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.
>
> > [!WARNING]
> >
> > **Experimental** — Web mode uses a reverse-engineered protocol. Your number may be banned by WhatsApp. Use Cloud API for production.

> [tab] Cloud API (Production)
>
> Uses the official Meta WhatsApp Business API.
>
> **1. Get Credentials**
>
> 1. Go to [Meta for Developers](https://developers.facebook.com/)
> 2. Create an app → add the **WhatsApp** product
> 3. Copy your **Phone Number ID** and **Access Token**
>
> **2. Set Environment Variables**
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> export WHATSAPP_ACCESS_TOKEN="EAAx..."
> export WHATSAPP_PHONE_NUMBER_ID="123456789"
> export WHATSAPP_VERIFY_TOKEN="my-secret"
> ```
>
> **3. Start**
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> praisonai bot whatsapp
> ```
>
> This starts a webhook server on port 8080. Point your Meta webhook URL to `https://your-domain.com/webhook`.
>
> > [!NOTE]
> >
> > WhatsApp **cloud** mode now stays running correctly. Previously its non-blocking start path exited `0` in about a second, so `systemd Restart=on-failure` never restarted it. The CLI now holds the loop open while the adapter is running, and startup failures exit **1**.

> [tab] Python SDK
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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())
> ```

> [!NOTE]
>
> **Multiple agents on one bot?** Route each inbound business number to a different agent with the [Agent Registry](https://praison.ai/docs/features/agent-registry.md) — one bot, N agents, dispatched by phone number.

***

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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])` |

> [!NOTE]
>
> **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

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Bot as WhatsAppBot
    participant Renderer as WhatsAppPresentationRenderer
    participant WA as Cloud API
    participant User

    Agent->>Bot: render_presentation(target, presentation)
    Bot->>Renderer: render(presentation)
    Renderer->>Renderer: adapt_presentation(whatsapp limits)
    Renderer-->>Bot: interactive payload (button / list)
    Bot->>WA: POST interactive message
    WA->>User: Tappable buttons / list
    User->>WA: Tap
    WA-->>Bot: Command callback
```

> [tab] Approval prompt (reply buttons)
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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())
> ```

> [tab] Menu / select (list message)
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> 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](https://praison.ai/docs/features/bot-presentations.md) 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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TD
    MSG["📩 Message"] --> CANON["🪪 Canonicalize\nLID → phone"]
    CANON --> STALE{"Older than\nconnect time?"}
    STALE -->|"Yes"| DROP["🗑️ Drop"]
    STALE -->|"No"| SELF{"Self-chat?\n(you → your number)"}
    SELF -->|"Yes"| PROCESS["✅ Process"]
    SELF -->|"No"| OUT{"Outgoing to\nsomeone else?"}
    OUT -->|"Yes"| BLOCK["❌ Block"]
    OUT -->|"No"| LIST{"In allowlist?"}
    LIST -->|"Yes"| PROCESS
    LIST -->|"No"| IGNORE["❌ Ignore"]

    classDef canon fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef no fill:#EF4444,stroke:#7C90A0,color:#fff

    class CANON canon
    class STALE,SELF,OUT,LIST check
    class PROCESS ok
    class DROP,BLOCK,IGNORE no
```

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](#identity-lid-vs-phone-jid).

### Four Layers of Protection

> [card] Stale Message Guard
>
> Messages older than when the bot connected are dropped. Prevents replaying old conversations on reconnect.

> [card] Self-Chat Check
>
> Only messages where sender = chat JID pass.

> [card] Outgoing Guard
>
> Your messages sent to other people's chats are always blocked — even if they're in the allowlist.

> [card] Allowlists
>
> Optionally 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>@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.

> [!NOTE]
>
> This uses the reusable `IdentityCanonicalizerProtocol` extension point. To plug canonicalization into another platform with multiple address forms, see [Identity Canonicalization](https://praison.ai/docs/features/gateway-identity-canonicalization.md).

### Expand Who Can Message the Bot

> [tab] Self-Only (Default)
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> praisonai bot whatsapp --mode web
> ```
>
> Only responds when you message your own number.

> [tab] Specific Numbers
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> # 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
> ```

> [tab] Specific Groups
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> # 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
> ```

> [tab] Everyone
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> praisonai bot whatsapp --mode web --respond-to-all
> ```

### Filtering 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](mailto:g@g.us) | ❌ | ❌ | ✅ | ✅ |
| Other group | ❌ | ❌ | ❌ | ✅ |
| Old/offline messages | ❌ | ❌ | ❌ | ❌ |

> [!NOTE]
>
> Phone numbers are normalized automatically — `+1-234-567-890` and `1234567890` match the same number.

### Python SDK Filtering

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai bot start --config bot.yaml
```

***

## Built-in Commands

| Command | Description |
| - | - |
| `/help` | Show available commands |
| `/status` | Bot info, model, uptime |
| `/new` | Reset conversation session |

Register custom commands:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
@bot.on_command("ping")
async def ping(msg):
    return "Pong!"
```

***

## CLI Options

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "WhatsApp Bot Runtime"
        CLI["praisonai bot whatsapp"] --> WB[WhatsAppBot]
        WB --> |"Web mode"| WA[WhatsAppWebAdapter]
        WB --> |"Cloud mode"| WH[Webhook Server]
        WA --> NC[neonize Client]
        NC <--> WS[WhatsApp Servers]
    end
    
    subgraph "Message Processing"
        WB --> FIL[Message Filter]
        FIL --> |"Pass"| SM[Session Manager]
        SM --> AG[AI Agent]
        AG --> |"Response"| WB
    end

    classDef bot fill:#8B0000,color:#fff
    classDef infra fill:#189AB4,color:#fff
    
    class CLI,WB,FIL,SM bot
    class WA,WH,NC,WS,AG infra
```

> [!NOTE]
>
> **Cloud-mode webhook verification:** inbound Meta webhooks are HMAC-verified via the shared helper. Set `WHATSAPP_APP_SECRET` (see [Messaging Bots](https://praison.ai/docs/features/messaging-bots.md)). Details: [Webhook Verification](https://praison.ai/docs/features/webhook-verification.md).

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 |

> [!NOTE]
>
> 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](https://praison.ai/docs/features/gateway-channel-supervision.md).

***

## Troubleshooting

> [accordion] 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> pip install --upgrade praisonai
> ```

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> rm -rf ~/.praisonai/whatsapp/
> ```

> [accordion] Session expired
>
> WhatsApp Web sessions expire if your phone is offline for 14+ days. Delete and re-scan:
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> rm -rf ~/.praisonai/whatsapp/
> praisonai bot whatsapp --mode web
> ```

***

## Best Practices

> [accordion] 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.

> [accordion] 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.

> [accordion] 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.

> [accordion] 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](https://praison.ai/docs/features/scheduler-delivery.md#out-of-process-delivery).

***

## Related

> [card] [Inbound Media](https://praison.ai/docs/features/bot-inbound-media.md)
>
> Full reference for inbound media: security pipeline, SSRF guard, and patterns

> [card] [Messaging Bots](https://praison.ai/docs/features/messaging-bots.md)
>
> All supported platforms: Telegram, Discord, Slack, WhatsApp

> [card] [WhatsApp MCP](https://praison.ai/docs/mcp/whatsapp.md)
>
> Send WhatsApp messages from agents using MCP tools

> [card] [Bot Commands](https://praison.ai/docs/features/bot-commands.md)
>
> Custom command registration and handling

> [card] [Approval Protocol](https://praison.ai/docs/features/approval-protocol.md)
>
> Human-in-the-loop tool approval for bots


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.