Talk to WhatsApp from Python or let an AI do it for you.
piwapp is a WhatsApp client written entirely in Python. You link it to your phone by scanning a QR code (just like WhatsApp Web), and from then on your code can send and receive messages, media, and group chats. It also ships an MCP server so assistants like Claude or GitHub Copilot can read and send your WhatsApp messages in plain English.
It is purely built on Python
No browser automation, no Node.js, no Go bridge. Pure Python, all the way down to the encryption.
Status: working and live-tested against real WhatsApp. QR login, staying online, sending/receiving 1:1 and group messages, media (images, video, audio, documents), chat-history sync, and the MCP server have all been verified against the real service.
- 🔗 Link your account with a QR code and stay connected (auto-reconnect).
- 💬 Send & receive text — direct chats and groups.
- 🖼️ Send & receive media — images, video, audio, documents (encrypted + uploaded/downloaded for you).
- 👥 Groups, first-class — list your groups, send to them, decrypt everyone's messages.
- 🗂️ Keep history — messages save to a local SQLite database you can query offline.
- 🤖 Hook it up to an LLM — the built-in MCP server lets Claude/Copilot chat on WhatsApp for you.
Requires Python 3.12+.
pip install piwapp # core library
pip install "piwapp[mcp]" # …or with the MCP server for LLMsInstalling also adds two command-line tools to your PATH:
piwapp— the QR-login client (same aspython -m piwapp)piwapp-mcp— the MCP server (same aspython -m piwapp.mcp_server)
From source (for development)
git clone https://github.com/n-92/piwapp && cd piwapp
pip install -e ".[dev]" # editable install + test deps
python -m pytest # ~147 testspiwapp # creates/uses ./piwapp_auth.jsonA QR code prints in your terminal (and saves to piwapp_qr.png). Scan it with
WhatsApp → Linked Devices → Link a Device. You'll see ✓ Online as <you> and
incoming messages will start printing. Next time you run it, it logs straight back
in — no QR needed.
import asyncio
from pathlib import Path
from piwapp import Client, ConnectionConfig, AuthenticationCreds
auth = Path("my_account.json")
def load_creds():
if auth.exists():
return AuthenticationCreds.from_json(auth.read_text())
creds = AuthenticationCreds.initial()
auth.write_text(creds.to_json())
return creds
async def main():
client = Client(
load_creds(),
ConnectionConfig(),
on_creds_update=lambda c: auth.write_text(c.to_json()), # save login
keys_path="my_account.json.keys", # save encryption keys
db_path="my_account.db", # save messages (optional)
)
client.on("connection.update", on_connection)
await client.start() # connects and reconnects until you stop it
async def on_connection(update: dict):
if "qr" in update:
print("Scan this QR:", update["qr"]) # or render/save it as an image
if update.get("connection") == "open":
print("online as", (update.get("me") or {}).get("id"))
asyncio.run(main())from piwapp.events import WAEventType
def on_messages(payload):
for m in payload.messages:
chat = m["key"]["remoteJid"] # who/which group it's from
sender = m["key"].get("participant") or chat
text = m.get("text") # decoded text (or caption)
if m.get("media"):
print(f"{sender} sent a {m['media']['type']}: {text!r}")
else:
print(f"{sender}: {text!r}")
client.events.on(WAEventType.MESSAGES_UPSERT, on_messages)# direct message — phone number in international format, no "+"
await client.send_text("15551234567@s.whatsapp.net", "hello from piwapp 🐍")
# a group — send_text auto-detects the @g.us address
await client.send_text("120363XXXXXXXXXXXX@g.us", "hi everyone!")
# list the groups you're in
groups = await client.fetch_groups() # [{id, subject, size, ...}, ...]# straight from a path (type is guessed from the extension)
await client.send_file("15551234567@s.whatsapp.net", "vacation.jpg", caption="📸")
# or from bytes you already have
await client.send_media("120363XXXXXXXXXXXX@g.us", img_bytes,
mimetype="image/png", caption="for the group")# `m` is one message dict from a MESSAGES_UPSERT payload
async def save_attachment(m):
if m.get("media"):
data = await client.download_media(m["message"]) # decrypts + verifies hashes
Path("downloaded.bin").write_bytes(data)If you passed db_path, every message is stored in SQLite. You can query it any
time — no connection required:
from piwapp.store import SqliteStore
db = SqliteStore("my_account.db")
db.last_sent_message() # the last thing you sent
db.recent_chats(20) # most recent conversations
db.get_chat_messages("120363...@g.us", 50) # recent messages in a chat
db.search_text("invoice") # find messages containing textThere's also a CLI helper: python scripts/query.py my_account.db.
This is the fun part. piwapp ships a Model Context Protocol server, so an assistant (Claude Code, Claude Desktop, GitHub Copilot agent mode, …) can use your WhatsApp with natural language: "text Mom I'm running late", "what did the team group say today?", "watch the group and reply to anyone who messages."
# 1. install with the MCP extra
pip install "piwapp[mcp]"
# 2. link your WhatsApp once — scan the QR it prints
piwapp-mcp --pair my.json
# creates my.json (+ .keys + .db) and prints the exact settings to use next
# 3. register it with your assistantClaude Code — one command, no files to edit:
claude mcp add piwapp \
-e PIWAPP_AUTH=my.json -e PIWAPP_DB=my.json.db \
-- piwapp-mcpClaude Desktop / VS Code (Copilot) — add this to the MCP config
(claude_desktop_config.json, or .vscode/mcp.json under a "servers" key):
{
"mcpServers": {
"piwapp": {
"command": "piwapp-mcp",
"env": {
"PIWAPP_AUTH": "/full/path/my.json",
"PIWAPP_KEYS": "/full/path/my.json.keys",
"PIWAPP_DB": "/full/path/my.json.db"
}
}
}
}Then just talk to your assistant:
"Send 'on my way' to +15551234567" "What are my most recent WhatsApp chats?" "Search my messages for 'invoice'" "Send this photo to the Family group: ~/Pictures/dog.jpg" "Watch the Test group and reply to whoever messages me"
| Tool | What it does |
|---|---|
start_pairing / pairing_status |
link a device by QR right inside the chat |
send_message |
send a text (direct or group) |
send_file |
send an image/video/audio/document from a path |
wait_for_messages |
wait for incoming messages (the "listen" half of a chat) |
list_chats / get_messages |
browse recent chats and their messages |
search_messages |
full-text search your history |
last_sent_message |
the last thing you sent |
list_groups / group_info |
your groups and their details |
download_media |
save media from a stored message to a file |
connection_status / archive_stats |
health + summary |
The archive tools work without a live connection; sending and wait_for_messages
need live mode (i.e. PIWAPP_AUTH set to a paired account).
Note on accounts & privacy. Your WhatsApp login lives only in your
my.json(+.keys/.db) — it's never shared or committed. Sharing piwapp means sharing the code; each person links their own phone. And run only one thing at a time against a given account file (the CLI,--pair, or the MCP server — not several at once), since WhatsApp allows a device a single live connection.
See USAGE.md for more detail and ready-to-paste configs.
WhatsApp Web speaks a custom binary protocol over a WebSocket, wrapped in two layers of encryption: a Noise handshake for the connection, and the Signal protocol (the same one Signal Messenger uses) for end-to-end message encryption. The Double Ratchet for direct chats and Sender Keys for groups. piwapp implements all of that natively in Python.
That's the differentiator. The other ways to use WhatsApp from Python either drive
a real browser (fragile), wrap a Go library like whatsmeow (ships a compiled
binary), or use the paid official Cloud API. piwapp is a self-contained, pure-Python
stack with no native bridge — easy to read, audit, and hack on — and it treats
group chat as a first-class feature.
Component status (click to expand)
| Area | Status |
|---|---|
| WebSocket transport + Noise XX handshake | ✅ tested |
| WABinary codec + token tables | ✅ tested |
| Signal: X3DH, Double Ratchet (1:1), Sender Keys (groups) | ✅ tested |
| QR pairing + login + auto-reconnect/keepalive | ✅ live-verified |
| Pre-key upload (enables receiving) | ✅ live-verified |
| Receive & decrypt (direct + group) | ✅ live-verified |
| Send 1:1 (to external contacts) | ✅ live-verified |
| Send to groups (sender-key fan-out, tested to 46 devices) | ✅ live-verified |
| Media send + receive | ✅ live-verified |
| Chat-history sync → SQLite archive | ✅ live-verified (83 chats / 576 msgs) |
| MCP server (read + live send + pairing + listen) | ✅ live-verified |
| Rich group-management APIs (history, join-requests, activity feed) | 🚧 planned |
pip install -e ".[dev]"
python -m pytest # ~147 tests, all offlineThe suite covers the binary codec (with property-based fuzzing), the full Noise
handshake, Signal 1:1 and group crypto (including out-of-order and tamper
rejection), a complete mock-server login + reconnect flow, the event/store layers,
end-to-end message encode/decode, media encrypt/upload/download, and the MCP tools.
A gated integration test (PIWAPP_TEST_REAL=1) does a real handshake with
web.whatsapp.com.
Next up are the rich group-management APIs — persistent group state with change history, batch member management, a join-request approval workflow, and an activity feed — plus retry receipts and app-state sync. That group tooling is piwapp's intended headline feature.
MIT
