# Whatsapp Web

> WhatsApp Web automation via Playwright + Chrome CDP. Use for sending/reading messages, managing chats and groups, contacts, media download, login/logout, and number verification. Triggers (EN or Indonesian): open/launch WhatsApp Web, send or read messages, check last reply, list/pinned/unread chats, check or batch-verify numbers, add/save contacts, pin/unpin, create/delete/exit groups, get profile, download media, and logout.

- Skill: `lazuardytech/whatsapp-web` (Agent Skill, multi-file: 36 files)
- Install (CLI): `npx skillmds@latest add lazuardytech/whatsapp-web`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lazuardytech/whatsapp-web/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: lazuardytech (https://skillmd.com/u/lazuardytech)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lazuardytech/whatsapp-web

---


## Setup Check

Before running any script, verify the skill is ready by running `scripts/login.py` (non-blocking). Check the `state` field:

- `logged_in` — ready, proceed with the task
- `qr_code` — not logged in, tell the user:
  > "WhatsApp Web isn't logged in yet. Open a terminal, run `python3 scripts/login.py`, scan the QR code from your phone, then try again."
- `error` — Chrome not found or CDP failed, tell the user:
  > "Couldn't connect to Chrome. Make sure Google Chrome is installed and Playwright is set up. See `SETUP.md` for instructions."

For first-time setup, direct the user to `SETUP.md`.

# whatsapp-web

WhatsApp Web automation via Playwright + Chrome CDP. Scripts output JSON to stdout.
Run CLIs from this skill's directory: `python3 scripts/<name>.py`.

## Setup

Requires Python 3.10+, Google Chrome, and Playwright.

First-time login — scan QR code once:

```bash
python3 scripts/login.py
```

This opens Chrome, navigates to web.whatsapp.com, reports the current login state, and **exits immediately** so the calling agent stays responsive. If the user still needs to scan the QR code, tell them to scan it from their phone and re-run the task once signed in. Chrome profile persists in `/tmp/whatsapp-web/chrome_profile/`, so no re-scan is needed after the first login.

## Available Scripts

### Check if number(s) are on WhatsApp

```bash
# Single number
python3 scripts/check_number.py --phone 081234567890

# Multiple numbers (comma-separated)
python3 scripts/check_number.py --phones 08111,08222,08333
```

Output: `{"081234567890": true}`

### Add a new contact

```bash
python3 scripts/add_contact.py --phone 081234567890 --first-name Ezra
python3 scripts/add_contact.py --phone 081234567890 --first-name Ezra \
    --last-name Wijaya --sync
```

Output: `{"status": "saved", "first_name": "Ezra", "last_name": "Wijaya", "phone": "081234567890", "sync_to_phone": true}`

**Agent must ask the user for First Name, Last Name (optional), and whether to sync the contact to the phone before invoking this script.** Pass `--sync` only if the user confirms syncing.

### Create a group

```bash
python3 scripts/create_group.py --name "LT Team" --members "Ezra,Adit,Rani"

# Members can be repeated — useful when the user provides them in batches
python3 scripts/create_group.py --name "LT Team" \
    --members "Ezra,081234567890" --members "Adit"
```

Output:

```json
{
  "status": "created",
  "name": "LT Team",
  "requested_members": ["Ezra", "Adit", "Rani"],
  "added": ["Ezra", "Adit"],
  "failed": ["Rani"]
}
```

`failed` lists members whose name/number didn't match a suggestion and were skipped. The group is still created as long as at least one member is added.

**Agent must ask the user for both the group name and the members.** Members can be many — accept comma-separated input and repeat the prompt if the user has more to add. Combine everything into one or multiple `--members` flags.

### Exit (leave) a group

```bash
python3 scripts/exit_group.py --name "LT Team" --confirm
```

Output: `{"status": "exited", "name": "LT Team", "exited": true, "already": false}`

If the menu has no Exit option (already left), returns `{"status": "noop", "exited": false, "already": true}`. The group stays visible in your chat list as read-only — use `delete_chat.py` afterwards to hide it.

**`--confirm` required.** Leaving is reversible only if an admin re-invites you.

### Delete a chat

```bash
python3 scripts/delete_chat.py --to "Ezra" --confirm
python3 scripts/delete_chat.py --to 081234567890 --confirm
```

Output: `{"status": "deleted", "name_or_number": "Ezra", "deleted": true}`

Removes the chat from YOUR sidebar and clears your copy of the history. The other party still sees the conversation. Not reversible.

For active groups WA won't offer "Delete chat" — use `exit_group.py` first, or `delete_group.py` for the full teardown.

**`--confirm` required.**

### Get your own profile

```bash
python3 scripts/get_profile.py
python3 scripts/get_profile.py --out profile.json
```

Output: `{"status": "ok", "name": "...", "about": "...", "photo_url": "..."}`

Reads the logged-in account's display name, About/status text, and profile photo URL (if WhatsApp exposes one). `photo_url` may be a `blob:` URL that doesn't outlive the session. Returns `{"status": "not_found"}` (exit 2) if the profile can't be read. Optional `--out` writes the JSON to a file instead of only stdout.

### Download media from a message

```bash
python3 scripts/download_media.py --from "Ezra" --message-id A1B2C3D4
python3 scripts/download_media.py --from 081234567890 --message-id A1B2C3D4 --out ./downloads
```

Output: `{"status": "ok", "chat": "Ezra", "message_id": "A1B2C3D4", "path": "./downloads/photo.jpg"}`

Downloads the image/video/document/audio attached to a specific message, identified by `--from` (contact name or phone) and `--message-id` (the WA message id — the trailing component of the message's `data-id`). Files land in `--out` (default `./downloads`). Exits 2 if the chat or message isn't found.

### Log out of WhatsApp Web

```bash
python3 scripts/logout.py --confirm
```

Output: `{"status": "logged_out"}`

Signs out the WhatsApp Web account only. The Chrome process is NOT closed or killed — it keeps running by design.

**`--confirm` required.** Agent must ask the user for explicit confirmation before passing `--confirm`.

### Delete a group (kick all → exit → delete)

```bash
python3 scripts/delete_group.py --name "LT Marketing Team" --confirm
```

Output:

```json
{
  "status": "deleted",
  "name": "LT Marketing Team",
  "kicked": ["Adit", "Rani"],
  "skipped": [],
  "exited": true,
  "deleted": true
}
```

`status` values:
- `deleted` — kicked all kickable members, exited the group, removed it from the chat list.
- `exited` — exit succeeded but delete didn't finalize (you can still remove it from the sidebar manually).
- `partial` — something stopped before exit.

`skipped` lists members whose Remove action didn't surface — usually means the caller isn't a group admin, so those members remain in the group.

**DESTRUCTIVE.** The script refuses to run without `--confirm`. Agent must ask the user for explicit confirmation before passing `--confirm`.

### Pin / unpin a chat

```bash
python3 scripts/pin_chat.py --to "Ezra"
python3 scripts/pin_chat.py --to "Ezra" --unpin
python3 scripts/pin_chat.py --to 081234567890
```

Output examples:
- `{"status": "pinned", "action": "pin", "name_or_number": "Ezra", "already": false}`
- `{"status": "noop", "action": "pin", "name_or_number": "Ezra", "already": true}` (already pinned)
- `{"status": "unpinned", "action": "unpin", ...}`

WhatsApp Web allows at most 3 pinned chats. If pinning a 4th, WA shows a modal the script auto-dismisses — the chat stays unpinned and `status` stays `unpinned` (tell the user to unpin something first).

### Send a message

```bash
python3 scripts/send_message.py --to "Ezra" --message "Hello!"
python3 scripts/send_message.py --to 081234567890 --message "Hi there"
```

Output: `{"status": "sent", "to": "Ezra"}`

### Read recent messages from a chat

```bash
python3 scripts/read_messages.py --from "Ezra"
python3 scripts/read_messages.py --from 081234567890 --count 20
```

Output:

```json
{
  "from": "Ezra",
  "count": 10,
  "messages": [
    {"direction": "in", "sender": "Ezra", "time": "08.42", "date": "17/04/2026", "text": "..."},
    {"direction": "out", "sender": "Me", "time": "08.43", "date": "17/04/2026", "text": "..."}
  ]
}
```

`direction` is `"in"` (received) or `"out"` (sent by the logged-in user).

### Last reply from a contact

```bash
# Last incoming message (what the contact said) — maps to "X bales apa"
python3 scripts/last_reply.py --from "Ezra"

# Last message regardless of sender — maps to "apa chat terakhir X"
python3 scripts/last_reply.py --from "Ezra" --any-direction
```

Output:

```json
{
  "from": "Ezra",
  "mode": "incoming",
  "message": {
    "direction": "in",
    "sender": "Ezra",
    "time": "08.42",
    "date": "17/04/2026",
    "text": "oke siap"
  }
}
```

`message` is `null` if no matching message is visible (e.g. asking for an incoming message in a chat the user has only sent to).

### List chats in the sidebar

```bash
python3 scripts/list_chats.py                 # top 50 chats
python3 scripts/list_chats.py --limit 20      # top 20
python3 scripts/list_chats.py --names-only    # drop previews
```

Output: `{"total_in_sidebar": 188, "returned": 50, "chats": [{"name": "...", "preview": "...", "pinned": false}, ...]}`

`total_in_sidebar` is the full chat count WhatsApp reports (all archived + active), `returned` is how many entries the script actually collected.

### List pinned chats

```bash
python3 scripts/list_pinned.py
```

Output: `{"count": 2, "chats": [{"name": "...", "preview": "...", "pinned": true}, ...]}`

WhatsApp Web allows at most 3 pinned chats.

### List unread chats / count unread messages

```bash
python3 scripts/list_unread.py                 # scan top 50 rows
python3 scripts/list_unread.py --limit 100     # scan deeper
python3 scripts/list_unread.py --count-only    # just the totals
```

Output:

```json
{
  "chat_count": 3,
  "message_count": 46,
  "chats": [
    {"name": "LT Marketing Team", "unread_count": 33, "unread": true, "pinned": false, ...}
  ]
}
```

`chat_count` = number of chats with unread messages; `message_count` = sum of per-chat unread counts. Only chats whose rows are scanned are counted — raise `--limit` if you have many chats and want to look deeper.

### Open WhatsApp Web / check login state

```bash
# Default: open WA Web, report state, exit immediately (non-blocking)
python3 scripts/login.py

# Block until the user signs in (only when explicitly requested)
python3 scripts/login.py --wait
python3 scripts/login.py --wait --timeout 120
```

Default mode exits right after opening the window — agents MUST NOT use `--wait` unless the user explicitly asks to wait for login, otherwise the agent will appear to hang while the user scans the QR code.

Output examples:
- `{"state": "logged_in"}`
- `{"state": "qr_code", "action": "Scan the QR code with your phone", "message": "..."}`
- `{"state": "loading", "message": "..."}`
- `{"state": "timeout", "error": "..."}` (only with `--wait`)
- `{"state": "error", "error": "..."}` — Chrome / CDP / navigation failed

`login.py` never crashes with a traceback: Chrome-launch, CDP-connect, and navigation failures are reported as `{"state": "error", ...}` with exit code 1, same as a `--wait` timeout.

## Script conventions

- All scripts output JSON to stdout, diagnostics to stderr
- Exit codes: `0` = success, `1` = login required / login error / wait timeout, `2` = contact not found, `3` = destructive script missing `--confirm`
- Destructive scripts (`delete_group.py`, `exit_group.py`, `delete_chat.py`, `logout.py`) refuse to run without `--confirm` (exit code 3). Agent MUST ask the user to confirm first before invoking them
- Run `python3 scripts/<name>.py --help` for usage
- Scripts use PEP 723 inline dependencies — run with `uv run` or install Playwright manually

## Important notes

- Chrome persists across runs — never killed by the skill
- Anti-ban delay (default 3s) between operations
- Phone formatting defaults to Indonesian numbers (+62)
- All interactions are keyboard/text-based (no CSS selectors) for resilience against WA Web DOM changes
- Number verification checks multiple phone format variants for accuracy

