---
name: whatsapp
description: "WhatsApp channel for your agent via Baileys. QR auth, messaging, voice transcription, channel and business modes."
---

# WhatsApp

## What This Is

Gives your agent a WhatsApp number. Connect via QR code, send and receive messages, handle voice notes, and switch between personal (channel) and business modes. Built on Baileys — no Meta Business API needed.

## Dependencies

None.

---

## How Responses Work

**Your text response IS the WhatsApp reply.** When you receive a message tagged with `[WhatsApp | ...]`, the supervisor takes whatever you respond with and sends it directly to WhatsApp. You do NOT need to use curl or `/api/channels/send` to reply — just respond normally.

**Do NOT use `/api/channels/send` to reply to incoming WhatsApp messages.** That endpoint is ONLY for proactive messages (during pulse, cron, or when you want to initiate a conversation). If you use it to reply, the person will get duplicate messages.

**Adjust your style for WhatsApp:** Keep messages shorter and more conversational than chat. No markdown headers, no code blocks unless asked. Think texting, not email.

### Routing is supervisor-enforced — you can't accidentally cross channels

Each incoming user message is tagged with a routing target at the moment it enters your conversation. The supervisor pins the agent's reply to whatever surface triggered it — chat-bubble messages land in chat, WhatsApp messages reply to the originating WhatsApp chat (group or 1:1). Concurrent inbounds from different surfaces during the same turn cannot leak into each other's replies. **You don't need to remember which channel you're talking on — the channel context tag (`[WhatsApp | ... | role | name]`) is the truth, and the supervisor handles delivery.**

If you ever see content in your context that doesn't match the surface you think you're on, trust the tag, not your assumption. Reply to what the tag says.

---

## How Messages Arrive

When a message arrives via WhatsApp, the supervisor wraps it with context:

```
[WhatsApp | 5511999888777 | customer | Alice]
Hi, I'd like to schedule an appointment.
```

The format is: `[WhatsApp | phone | role | name (optional)]`

- **role=admin**: This is your human or an authorized admin. Use your normal personality, full capabilities, main system prompt.
- **role=customer**: This is someone else messaging. Follow the instructions from the active skill's SCRIPT.md (loaded as your system prompt for that conversation).
- **role=assistant**: Your human triggered you with `@botname:` inside a conversation with this person. You have the full conversation history as context. Execute the task and respond concisely — your reply goes directly into that chat. The SCRIPT.md from the active skill is loaded as your system prompt.

---

## Channel Config

Your channel configuration is injected into your context (if any channels are configured). It comes from `~/.bloby/config.json` — a file OUTSIDE your workspace that the supervisor manages.

---

## Modes

The mode determines the routing/security policy for inbound messages. The core (supervisor) enforces it — your skill is what decides which mode is active. Switch via `/api/channels/whatsapp/configure`.

**Channel Mode** (default): Your human's own WhatsApp number. Only self-chat (messages your human sends to themselves) triggers you — messages from other people are completely ignored. This is "just talk to me" mode. Group chats are always ignored in channel mode regardless of `allowGroups`.

**Business Mode**: Bloby has its own dedicated WhatsApp number. Numbers in the `admins` array get admin access (main system prompt). Everyone else is a customer and gets the support prompt from the active skill's SCRIPT.md.

**Assistant Mode**: Your personal assistant inside your own conversations. Self-chat works as a normal admin channel. When other people message you, their messages are silently stored for context. When YOU type `@botname:` followed by a command in someone's chat, the agent activates with full conversation context and responds in that chat. The trigger uses the bot's configured name (from `config.json` `username` field) and is case-insensitive. By default nobody else can trigger the agent — only you (the account owner). (This can be opened up with the dangerous `allowOthersToTrigger` flag — see the disclaimer under Setup.) Uses the active skill's SCRIPT.md for the system prompt and `customer_data/` for per-contact memory.

### Group chats (`allowGroups`)

Group chats are **off by default**. Enable per-channel with the `allowGroups` flag on `/configure`. When enabled:

- **Assistant mode**: messages in groups your human is part of are stored as conversation context (per-group, shared buffer). Your human can `@botname:` inside a group and you'll respond in that group with the full group context.
- **Business mode**: messages from group members are routed by `admins` (admin vs customer) like any other inbound. The group itself becomes the reply target.
- **Channel mode**: groups are always ignored — channel mode is self-chat only.

Reply target: in groups your reply goes to the group (everyone sees it), not to the individual sender. Per-group memory under `customer_data/` is keyed by the group's WhatsApp ID (not a phone number).

Toggle with:

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" \
  -d '{"allowGroups":true}'
```

Disable again with `{"allowGroups":false}`. Default is false — leave it off unless your human asks for it.

---

## Setup

### 1. Connect WhatsApp

When your human asks to configure WhatsApp:

1. Start the connection:
```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/connect
```

2. Tell them to open the QR page: `/api/channels/whatsapp/qr-page`
   (Send this as a relative URL — their browser is already on the correct domain. Don't mention the URL until you are actually starting the connection)

3. They scan the QR with their WhatsApp app

4. The default mode is **channel** (self-chat only)

If the QR page doesn't load, make sure you initiated the connection first (step 1).

**On mobile?** The QR page also offers a "Link with phone number instead" option. The user enters their phone number, gets an 8-character code, and types it into WhatsApp (Settings > Linked Devices > Link a Device > "Link with phone number instead"). No camera needed.

### 2. Choose a Mode

**Channel mode** (default) — personal assistant. Only self-chat triggers the agent:

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" \
  -d '{"mode":"channel"}'
```

**Business mode** — customer-facing. The agent responds to incoming messages from customers using a skill's SCRIPT.md. Admin numbers get full agent access:

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" \
  -d '{"mode":"business","admins":["ADMIN_PHONE_1","ADMIN_PHONE_2"],"skill":"SKILL_FOLDER_NAME"}'
```

Replace `ADMIN_PHONE_1` with the human's phone number (digits only, with country code, e.g. `5511999887766`). Replace `SKILL_FOLDER_NAME` with the skill that should handle customer conversations (e.g. `whatsapp-clinic-secretary`).

**Assistant mode** — personal assistant in your conversations. Self-chat works normally. Other people's messages are stored silently. Type `@botname: <command>` in any chat to trigger the agent:

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" \
  -d '{"mode":"assistant","skill":"SKILL_FOLDER_NAME"}'
```

The trigger uses the bot's name from `config.json` `username` field (e.g. if username is "bloby", trigger is `@bloby:`). By default **only the account owner can trigger** — other people's messages are context only.

To also let assistant mode operate in group chats your human is in, add `"allowGroups":true` to the configure call (see "Group chats" above).

### ⚠️ Shared control — `allowOthersToTrigger` (DANGEROUS)

By default, **only your human** (the account owner) can drive the agent with `@botname:`. Everyone else's messages are stored as context but cannot trigger anything.

Setting `allowOthersToTrigger: true` changes that: **anyone** who tags `@botname:` — in a 1:1 chat or in any group your human is in — can drive the agent.

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" \
  -d '{"mode":"assistant","allowOthersToTrigger":true}'
```

**Before enabling this, you MUST warn your human, in your own words, that this is dangerous:** anyone who can tag the bot gains control of an agent that can run Bash, read and write files, send messages, spend money, and act with your human's full permissions. A stranger added to a group — or anyone who learns the bot's name — could issue commands. There is no per-command confirmation.

It exists for genuinely trusted shared use — e.g. sharing one assistant with a partner. Do not enable it on a whim. Confirm explicitly with your human first, and remind them it can be turned off at any time:

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" -d '{"allowOthersToTrigger":false}'
```

Default is `false`. Leave it off unless your human clearly understands the risk and asks for it.

### 3. Verify

```bash
curl -s http://localhost:7400/api/channels/status
```

Expected: `"channel":"whatsapp","connected":true`

---

## Business Mode — Active Skill

Only ONE skill can be active for customer-facing mode at a time. The active skill is set in the channel config (`channels.whatsapp.skill`). When your human asks to switch skills:

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
  -H "Content-Type: application/json" -d '{"skill":"whatsapp-clinic"}'
```

The active skill should have:
- `SCRIPT.md` — the customer-facing system prompt (loaded automatically for customer conversations)
- Optionally a `customer_data/` directory — per-customer memory files (named by phone number, e.g. `5511999887766.md`)

---

## Sending Proactive Messages

To INITIATE a WhatsApp message (during pulse, cron, or when you want to reach out first):

```bash
curl -s -X POST http://localhost:7400/api/channels/send \
  -H "Content-Type: application/json" \
  -d '{"channel":"whatsapp","to":"5511999888777","text":"Your appointment is confirmed for tomorrow at 2pm."}'
```

Phone number format: digits with country code (e.g. `5511999887766`). The system normalizes to WhatsApp JID format automatically.

**Remember:** This is ONLY for starting new conversations or sending unprompted messages. When replying to an incoming message, just respond normally — the supervisor handles delivery.

---

## Customer Conversation Logs

When you finish a conversation with a **customer** via WhatsApp, save a summary to `whatsapp/{phone}.md`:
- Key details from the conversation
- Outcome (appointment scheduled, question answered, etc.)
- Any follow-ups needed
- Timestamp

This is your memory of that customer. Next time they message, read their file first.

---

## Voice Notes

Voice messages are automatically transcribed via Whisper and delivered as text. No extra setup needed if Whisper is configured on the supervisor.

## Typing Indicator

The agent automatically shows "typing..." to the recipient while composing a response. This is handled by the supervisor — no action needed from you.

## Message Buffering (Business Mode)

In business mode, rapid messages from the same customer are debounced (4-second window) and delivered together. The system maintains a 30-message conversation buffer per customer.

## Concurrent Conversations (Business Mode)

Up to 5 customer conversations can run in parallel. Additional messages queue automatically.

---

## Reactions (emoji ack)

Use a reaction when you want to acknowledge a message without sending a full text reply — perfect for "I saw this, working on it" or quick approvals/disapprovals. Reactions are first-class in WhatsApp and don't clutter the chat.

```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/react \
  -H "Content-Type: application/json" \
  -d '{"chatJid":"5511999888777@s.whatsapp.net","messageId":"3EB0...","fromMe":false,"emoji":"👀"}'
```

- `chatJid` — the WhatsApp JID where the message lives (group JID for groups, peer JID for 1:1). Include the suffix (`@s.whatsapp.net` or `@g.us`).
- `messageId` — the Baileys message id (the supervisor logs this on every inbound: look for `id=...` in the channel logs).
- `fromMe` — true if the message you're reacting to was sent by your number, false otherwise. Defaults to false.
- `participant` — optional; for group messages where you need to reference the original sender's JID.
- `emoji` — any single emoji. Pass `""` (empty string) to remove a previous reaction.

**When to react vs reply:** react for quick "saw it / on it / done" signals during long-running work; reply with text when there's something meaningful to say. Don't react AND reply with the same content — pick one.

---

## Account Management

**Disconnect** (keep credentials for later):
```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/disconnect
```

**Logout** (delete credentials, requires new QR scan):
```bash
curl -s -X POST http://localhost:7400/api/channels/whatsapp/logout
```

**Switch accounts** (relink): Use the "Relink" button on the QR page, or logout + connect again.

---

## Human Interaction

- The human must scan the QR code with their phone — this cannot be automated
- If WhatsApp disconnects (phone lost, account switched), the human needs to re-scan
- In business mode, explain to the human that admin numbers get full agent access while all other numbers get the customer-facing skill
- If the human asks about privacy: credentials are stored locally at `~/.bloby/channels/whatsapp/auth/`, never sent to external servers

---

## API Reference

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/channels/status` | GET | List all channel statuses |
| `/api/channels/whatsapp/qr` | GET | Get current QR code SVG |
| `/api/channels/whatsapp/qr-page` | GET | Standalone QR scanning page |
| `/api/channels/whatsapp/connect` | POST | Start WhatsApp (triggers QR if needed) |
| `/api/channels/whatsapp/disconnect` | POST | Disconnect WhatsApp |
| `/api/channels/whatsapp/logout` | POST | Disconnect + delete credentials |
| `/api/channels/whatsapp/configure` | POST | Set mode + admins + skill |
| `/api/channels/whatsapp/pairing-code` | POST | Get 8-char pairing code (mobile linking) |
| `/api/channels/whatsapp/react` | POST | Send/remove an emoji reaction on a message |
| `/api/channels/send` | POST | Send proactive message via channel |

All endpoints use `http://localhost:7400` for internal API calls (curl from your terminal). For URLs shown to your human, use relative paths (e.g. `/api/channels/whatsapp/qr-page`) — their browser is already on the correct domain.

---

## Technical Notes

- Baileys is a reverse-engineering of WhatsApp Web. It can break if WhatsApp changes their protocol. Reconnection is automatic on network drops.
- If you get error 401 (loggedOut), credentials were invalidated — the human needs to re-scan QR.
- If you get error 440 (connectionReplaced), another device/instance took over — do NOT auto-reconnect, ask the human.
- LID (Local ID) vs phone number: WhatsApp uses internal IDs. The system translates them automatically — use phone numbers in all your API calls.
