Skip to main content

WhatsApp client for Python, powered by Rust. A thin PyO3 wrapper over whatsapp-rust.

Project description

wars

WhatsApp client for Python, powered by Rust. A thin PyO3 wrapper over whatsapp-rust. Drop it into any Python app to send and receive WhatsApp messages — no Node.js sidecar, no separate server, no IPC.

wars is an unofficial library — it is not built by, affiliated with, endorsed by, or sponsored by WhatsApp / Meta. "WhatsApp" and related trademarks belong to their respective owners.

pip install wars

Why

  • No Node.js required. A single pip install ships native code; the WhatsApp Web protocol (Noise handshake, Signal Protocol, protobuf) runs in Rust inside your Python process.
  • Sync API. Call wa.send(...) from any Flask handler. No asyncio, no background server, no IPC.
  • No files by default. In-memory session out of the box — zero filesystem permissions to set. When you're ready, dump the session as bytes and stash it in your existing database.
  • One unified send(). Same call for text, images, documents, groups, and broadcasts. Bot mode with an @on_message decorator.

Quick start

1. Pair your phone (one-time)

Grab the example script from the repo and run it:

curl -O https://raw.githubusercontent.com/marketcalls/wars/main/examples/pair.py
python pair.py --phone 919876543210

Prints a QR in the terminal and, if WhatsApp accepts the request, an 8-character pair code. Use either:

  • QR: WhatsApp on phone → Linked devicesLink a device → scan.
  • Code: Linked devicesLink with phone number → type the code.

By default nothing is written to disk — the session is in-memory and will be lost when the script exits. For persistence pick one:

python pair.py --phone 919876543210 --db whatsapp.db    # SQLite file
python pair.py --phone 919876543210 --save-png qr.png   # also dump QR PNG

For the cleanest production flow, leave the script in-memory and stash the session bytes into your own DB — see Persist the session below.

2. Send a message

One unified send() for every pattern. Pass owner= once and single-arg sends go to your own number — perfect for personal alerts.

from wars import WhatsApp

# No db_path → in-memory session. Re-pair if the process restarts.
wa = WhatsApp(owner="14155550100")
wa.connect()
wa.wait_until_ready()

# Single-arg send → goes to owner (yourself)
wa.send("Hello from wars")
wa.send(f"Build #{build_id} finished in {elapsed:.1f}s")

# Send to a specific contact
wa.send("14155550199", "Are you free for a quick call?")

# Image with caption
wa.send("14155550100", image="screenshot.png", caption="Latest dashboard")

# Document
wa.send("14155550100", document="report.pdf")

# Group message
wa.send_group("120363012345678901@g.us", "Daily standup in 5 minutes")

# Broadcast the same body to many recipients
wa.send([(n, "Server maintenance starting now") for n in oncall])

3. Persist the session in your own database

In-memory is simplest but you re-pair on every restart. For production, keep the paired session as bytes inside your existing app database, an encrypted column in a secret store, or anywhere you already keep API keys.

import os, sqlite3, tempfile

# First run — pair to a short-lived temp file, then dump bytes once.
# (export_session() needs a file-backed DB; Rust + Python use separate
# SQLite library instances so an in-memory DB can't be snapshotted from
# Python. The temp file is 0600 and deleted as soon as we have the bytes.)
fd, pair_db = tempfile.mkstemp(suffix=".db", prefix="wars_pair_")
os.close(fd); os.chmod(pair_db, 0o600)

wa = WhatsApp(pair_db, owner="919876543210")
wa.connect(); wa.wait_until_ready()              # scan QR
blob = wa.export_session()                       # ~300 KB bytes
wa.disconnect()
os.unlink(pair_db)                               # clean up

# Stash wherever — example: a tiny table in your app's SQLite
db = sqlite3.connect("app.db")
db.execute("CREATE TABLE IF NOT EXISTS wa (id INTEGER PRIMARY KEY, blob BLOB)")
db.execute("INSERT OR REPLACE INTO wa VALUES (1, ?)", (blob,)); db.commit()

# Every run after — load and resume. No QR.
blob = db.execute("SELECT blob FROM wa WHERE id = 1").fetchone()[0]
wa = WhatsApp.from_bytes(blob, owner="919876543210")
wa.connect()                                     # instant reconnect

See examples/webapp_integration.py for the full pattern including a pair CLI subcommand, lazy singleton, and Flask integration.

4. Show the pairing QR in a browser (no files)

For a browser-based pairing screen, convert the QR to a base64 data URL and embed it in HTML — no PNG ever touches the disk:

from wars import WhatsApp, qr_to_data_url

wa = WhatsApp(owner="919876543210")
latest_qr = {"data_url": None}

@wa.on_qr
def cache(code):
    latest_qr["data_url"] = qr_to_data_url(code)

wa.connect()

# Flask route
@app.get("/pair-qr")
def pair_qr():
    return {"img": latest_qr["data_url"]}      # JSON for SPAs

# Or render directly:
#   <img src="{{ qr.data_url }}" />            # works because it's a data URL

Also available: qr_to_base64(code) for the raw base64 string without the data: prefix. Both require the qrcode extra (pip install wars[qr]).

5. Or: just use a file path

If you want SQLite-on-disk (simpler than DB-stored bytes for solo runs), pass a path:

wa = WhatsApp("whatsapp.db", owner="919876543210")

The file lives wherever the path points. Treat it like any other secret (restrict permissions, exclude from git — .gitignore already covers *.db).

6. Receive messages (bot mode)

from wars import WhatsApp, Message

wa = WhatsApp("whatsapp.db", owner="14155550100")

@wa.on_message
def handle(msg: Message):
    if msg.is_from_me:
        return  # ignore echoes of our own sends
    if msg.text == "/ping":
        wa.send(msg.chat, "pong")
    elif msg.text == "/status":
        wa.send(msg.chat, f"Uptime: {get_uptime()}")
    elif msg.text.startswith("/echo "):
        wa.send(msg.chat, msg.text.removeprefix("/echo "))

wa.connect()
wa.run_forever()

7. Integrate into a web app

Singleton pattern — one connection per process, send from any route. See examples/webapp_integration.py for the full sketch.

# yourapp/whatsapp.py
import atexit
from threading import Lock
from wars import WhatsApp

_wa: WhatsApp | None = None
_lock = Lock()
OWNER = "14155550100"

def wa() -> WhatsApp:
    """Lazy singleton — first call pairs/connects, rest are free."""
    global _wa
    if _wa is not None:
        return _wa
    with _lock:
        if _wa is not None:
            return _wa
        client = WhatsApp("whatsapp.db", owner=OWNER)
        client.connect()
        client.wait_until_ready(timeout=60)
        atexit.register(client.disconnect)
        _wa = client
        return _wa
# any Flask blueprint
from yourapp.whatsapp import wa

@app.post("/webhook/build")
def build_done():
    data = request.json
    wa().send(f"Build #{data['id']} {data['status']} in {data['duration']}s")
    return "ok"

Works the same way under Django (call from AppConfig.ready()), FastAPI (wrap blocking calls with asyncio.to_thread(wa().send, ...)), Streamlit, Dash, etc.

⚠️ One process only. WhatsApp Web is one-device-per-session. Run gunicorn -w 1 --threads 8 or equivalent — don't fork multiple workers all using the same whatsapp.db, or Meta will unlink the device.

API

WhatsApp(db_path=None, owner=None, log_level=None)

Construct a client. Does not connect.

  • db_path=None (default) — in-memory session, no filesystem touched.
  • db_path="path.db" — SQLite on disk, session survives restarts.
  • owner= — default recipient for single-arg send() calls.

WhatsApp.from_bytes(blob, owner=None)

Class method. Restore a session previously exported with export_session(). Use this to load a paired session from your own database.

Method Returns Notes
connect(phone=None) None Start background run loop. Optional E.164 digits enable pair-code auth.
wait_until_ready(timeout=120) None Block until paired+online. Raises TimeoutError.
is_connected() bool
disconnect() None Idempotent.
export_session() bytes Dump session for safe storage in your own DB / secret manager.
send(*args, image=, document=, caption=, filename=) message_id or list Unified API — see shapes below.
send_group(group_id, text) message_id: str Convenience for groups. group_id accepts "…@g.us" or bare digits.
send_text(to, text) message_id: str Explicit form. to accepts "919876543210", "+91 98765 43210", full JID, or group JID.
send_image(to, data, caption=None) message_id: str Explicit form. data: file path or bytes. MIME auto-sniffed.
send_document(to, data, filename=None, mimetype=None) message_id: str Explicit form.
on_qr(fn) decorator fn(qr_data: str)
on_pair_code(fn) decorator fn(code: str)
on_message(fn) decorator fn(msg: Message)
on_connected(fn) / on_disconnect(fn) decorator fn()
messages(timeout=1.0) iterator yields Message
events(timeout=1.0) iterator yields raw dicts
run_forever() None Block until Ctrl-C
print_qr(code) (static) None Render QR to stdout (terminal).
qr_to_base64(code) (static) str QR → base64 PNG. No filesystem I/O.
qr_to_data_url(code) (static) str QR → data:image/png;base64,… URL.

send() shapes

wa.send("alert text")                          # → owner
wa.send("919876543210", "alert text")          # → recipient
wa.send("919876543210", image="screenshot.png", caption="Dashboard")
wa.send("919876543210", document="report.pdf")
wa.send("120363…@g.us", "group msg")           # group JIDs work as-is
wa.send([(n, "msg") for n in subscribers])     # broadcast

Message (dataclass)

chat: str           # JID of the chat (1:1 or group)
sender: str         # JID of the person who sent it
is_group: bool
is_from_me: bool
id: str             # message ID
push_name: str      # sender's display name
timestamp: int      # unix seconds
text: str | None    # plain text content if any
media_type: str     # "" / "image" / "document" / ...

Building from source

git clone https://github.com/jlucaso1/whatsapp-rust
cd whatsapp-rust/python
uv venv && source .venv/bin/activate
uv pip install maturin
maturin develop --release        # build + install into current venv

The first build takes ~5 min (compiles the whole workspace); incremental builds are seconds.

To produce a wheel:

maturin build --release          # wheel lands in target/wheels/

To publish to PyPI:

maturin publish                  # needs PYPI_TOKEN

Wheels target Python 3.8+ via abi3 — one wheel per OS+arch covers every Python version from 3.8 onwards.

What's in v0.1

  • ✅ QR + pair-code auth, persistent SQLite session
  • ✅ Unified send() — text, image, document, groups, broadcast
  • send_group(), send_text(), send_image(), send_document() (explicit forms)
  • ✅ Receive messages via @on_message callback or wa.messages() iterator
  • ✅ All connection events: on_qr, on_pair_code, on_connected, on_disconnect
  • ✅ Built-in JID normalization (digits, +91 …, full JIDs, group JIDs)

Not in v0.1 (the Rust crate supports them; bindings to be added):

  • ❌ Group create/manage, reactions, edit/revoke, status, polls
  • ❌ Voice notes, video, contact lookup, presence
  • ❌ Async/await API (use asyncio.to_thread for now)

PRs welcome.

Credits

wars is a thin Python binding layer. The protocol work — Noise handshake, Signal Protocol, binary stanza encoding, media encryption, every byte that goes on the wire — is done by the projects below. If wars is useful to you, please go support them:

  • whatsapp-rust by @jlucaso1 — the Rust client that this package wraps. Without it, wars is nothing.
  • Baileys by the WhiskeySockets maintainers — the TypeScript reference implementation that whatsapp-rust learns from for protocol quirks, edge cases, and behavior parity.
  • whatsmeow by @tulir — the Go implementation that pioneered much of the multi-device protocol reverse-engineering.
  • @pokearaujo and @Sigalor for early observations of the WhatsApp Multi-Device and WhatsApp Web protocols that the above projects were built on.

The Python bindings layer (this package) is by Rajendran R / Marketcalls.

License

Copyright (c) 2026 Rajendran R / Marketcalls

Licensed under the MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Thus, the maintainers of the project can't be held liable for any potential misuse of this project.

wars also bundles and statically links the upstream whatsapp-rust crate, also distributed under the MIT License (Copyright (c) 2025 João Lucas de Oliveira Lopes). See LICENSE for the full combined notice.

Disclaimer

This is an unofficial, open-source reimplementation. Using custom WhatsApp clients may violate Meta's Terms of Service and could result in account suspension. Use at your own risk.

This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. The official WhatsApp website can be found at whatsapp.com. "WhatsApp" and related marks are registered trademarks of their respective owners.

The maintainers of wars do not in any way condone the use of this package in practices that violate the Terms of Service of WhatsApp. We call upon the personal responsibility of users to use this package fairly, as it is intended to be used. Do not spam people with this. We discourage any stalkerware, bulk, or automated mass-messaging usage.

WhatsApp Terms of Service — practical risk note

Unofficial WhatsApp clients can get the linked device unlinked or the entire account banned by Meta's automation. The dominant trigger is send volume and pattern, not the client itself:

  • Low risk (typical personal/automation usage) — a handful of notifications a day, the occasional /status reply to your own number or a small private group. This pattern is indistinguishable from a person using WhatsApp normally and stays well under Meta's automated thresholds.
  • Medium risk — sending to dozens of distinct contacts who haven't messaged you first, frequent broadcast lists, sending the same message body to many recipients in a short window.
  • High risk (don't) — bulk marketing, cold outreach to numbers you scraped, lookalike-spam patterns, evading rate limits. This is what triggers bans. Use the official WhatsApp Business / Cloud API for those use cases.

Treat your paired session file (whatsapp.db) as sensitive — it contains the private keys for your linked device. Anyone who gets a copy can impersonate that session.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

wars-0.1.0-cp38-abi3-win_amd64.whl (7.0 MB view details)

Uploaded CPython 3.8+Windows x86-64

wars-0.1.0-cp38-abi3-manylinux_2_28_x86_64.whl (6.7 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.28+ x86-64

wars-0.1.0-cp38-abi3-manylinux_2_28_aarch64.whl (6.2 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.28+ ARM64

wars-0.1.0-cp38-abi3-macosx_11_0_arm64.whl (6.1 MB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

wars-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl (6.5 MB view details)

Uploaded CPython 3.8+macOS 10.12+ x86-64

File details

Details for the file wars-0.1.0-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: wars-0.1.0-cp38-abi3-win_amd64.whl
  • Upload date:
  • Size: 7.0 MB
  • Tags: CPython 3.8+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wars-0.1.0-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 d2dfc06d7714f9893bfb2bd7ef30b00004e426a30d40643e0f9b2a033ef76033
MD5 e388357c61506162d1aa722d1d363ed0
BLAKE2b-256 566b410ba6a442e1cfe1672130a8d8ad654b8cbb031a02464908eca32210bc43

See more details on using hashes here.

Provenance

The following attestation bundles were made for wars-0.1.0-cp38-abi3-win_amd64.whl:

Publisher: release.yml on marketcalls/wars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wars-0.1.0-cp38-abi3-manylinux_2_28_x86_64.whl.

File metadata

  • Download URL: wars-0.1.0-cp38-abi3-manylinux_2_28_x86_64.whl
  • Upload date:
  • Size: 6.7 MB
  • Tags: CPython 3.8+, manylinux: glibc 2.28+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wars-0.1.0-cp38-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 210edfa2986bcaa04825e284b05e7fe6a4e557519c1e6d4e63aa7c10a2a5f1ab
MD5 4822f56ac2729028a186421a83ad8fb0
BLAKE2b-256 f2b443f417a0879d8b8ff58d445fab9a72bab26852ea2e56057d128a60a1b4c4

See more details on using hashes here.

Provenance

The following attestation bundles were made for wars-0.1.0-cp38-abi3-manylinux_2_28_x86_64.whl:

Publisher: release.yml on marketcalls/wars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wars-0.1.0-cp38-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for wars-0.1.0-cp38-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 4ea2e59dc8dc64e24682ab194d1ff41737291085506f4182840286cecbee054c
MD5 9abd969c9e7f5e967de9b32186b64f1c
BLAKE2b-256 a41d88ccb7c41e1c811f6fb218121694b48724d5f5b1f54a3708e3421f3a2d8f

See more details on using hashes here.

Provenance

The following attestation bundles were made for wars-0.1.0-cp38-abi3-manylinux_2_28_aarch64.whl:

Publisher: release.yml on marketcalls/wars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wars-0.1.0-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

  • Download URL: wars-0.1.0-cp38-abi3-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 6.1 MB
  • Tags: CPython 3.8+, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wars-0.1.0-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f906c49864563ceab1d867223379788da62996697bd051b7b3d5d2cd690b0721
MD5 e25d3850bf467a9093068ae12ec64f00
BLAKE2b-256 6b0033b425b6cb9fca39ace28e7f8395d6f904230452910d420fea12c90919e7

See more details on using hashes here.

Provenance

The following attestation bundles were made for wars-0.1.0-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on marketcalls/wars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wars-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl.

File metadata

  • Download URL: wars-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl
  • Upload date:
  • Size: 6.5 MB
  • Tags: CPython 3.8+, macOS 10.12+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wars-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 81591a7e58754fc64c5505425f9bd64ed44bf7f76a3bf47885dad8b81ab77129
MD5 4cbd82424c3cb3537171e8eae53f7b86
BLAKE2b-256 efeb6d0f021cf879e4401d492309096e95c81e0a91f5d537371ad892f00995f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for wars-0.1.0-cp38-abi3-macosx_10_12_x86_64.whl:

Publisher: release.yml on marketcalls/wars

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page