An async-first Python client for the WhatsApp account you link. Register events, send messages, work with media and groups, and reuse named sessions.
pip install whatsapp-bridgeVersion 1.0 is a breaking rewrite. The original 0.1 codebase does not have this API. Existing users should read the migration guide and 1.0.0 release notes before upgrading.
from whatsapp_bridge import WhatsAppClient, events
client = WhatsAppClient("personal")
@client.on(events.NewMessage(incoming=True))
async def handler(event):
print(event.sender, event.text)
if event.text.lower() == "ping":
await event.reply("pong")
client.run_until_disconnected()On first connection, scan the terminal QR using WhatsApp β Linked Devices β Link a Device. Subsequent connections reuse the saved linked-device identity, unless WhatsApp removes or invalidates it. There are no separate WhatsApp bot accounts: all actions use the account you explicitly link.
This uses unofficial WhatsApp Web linked-device functionality, not the WhatsApp Business API. It is not affiliated with WhatsApp or Meta. Protocol changes, account restrictions, privacy settings, and WhatsApp permissions can affect availability. Use automation only with appropriate consent.
Python 3.11β3.14. Release wheels target Windows x86-64, macOS 13+ Intel/Apple
Silicon, and Linux x86-64 (glibc 2.17+ build target). The native Go shared library and SQLite are bundled
in each wheel. The only separately installed Python runtime dependency is
segno for QR rendering.
Installed wheels require no Go, Git, C compiler, Docker, HTTP bridge, or browser. There are no runtime downloads of executables. Unsupported platforms fail with a clear native-library error. Source builds require developer toolchains; supported wheel platforms and actual verification are tracked in validation.
The backend is a small owned in-process whatsmeow adapter, isolated behind a private interface. The requested Neonize option was audited and rejected for specific native lifecycle and crash defects in its published release. See the repository and backend audit for versions, source links, and the comparison with other in-process options.
The linked-device name defaults to Sree, configured once in
src/whatsapp_bridge/config.py. Override it per client without editing the backend:
from whatsapp_bridge import ClientConfig, WhatsAppClient
client = WhatsAppClient("personal", config=ClientConfig(device_name="My Bridge"))This name is sent when pairing; it is separate from the local session name
(personal). To change an existing device's displayed name, unlink that device
in WhatsApp and pair again with the new configuration. WhatsApp controls the
final display and icon.
import asyncio
from whatsapp_bridge import WhatsAppClient
async def main():
async with WhatsAppClient("personal") as client:
message = await client.send_message("+919876543210", "Hello!")
print(message.id)
asyncio.run(main())connect() waits for authentication. disconnect() closes resources and keeps
the identity. logout() explicitly unlinks the account. me holds normalized
device information after connection. is_connected and connection_state
describe local state; send acknowledgement is not a recipient read receipt.
For a long-running async application:
async with WhatsAppClient("personal") as client:
await client.wait_until_disconnected()whatsmeow owns network reconnects and increasing retry delays. This package adds a finite reconnect deadline, stops on permanent authentication failures, and cancels reconnects during shutdown. Sends are never automatically retried: a timeout can mean WhatsApp accepted a message but its acknowledgement was lost.
run_until_disconnected() is the blocking script entry point. Use the async API
inside notebooks, web frameworks, and existing loops. There is no separate sync
client implementation. Keep a client on the loop where it first connected;
create a new client instance when creating a new loop.
@client.on(
events.NewMessage(
incoming=True,
private=True,
from_users=["+919876543210"],
pattern=r"^hello\b",
has_media=False,
)
)
async def greeting(event):
await event.reply("Hey!")Filters: incoming, outgoing, private, groups, from_users, chats,
pattern, and has_media. Booleans require the specified value. Patterns use
re.search, and the match is available as event.pattern_match. Filter entities
are numbers/JIDs, not display names. Contradictory flags are rejected.
Events: NewMessage, MessageEdited, MessageDeleted, Reaction, Receipt,
Presence, Typing, Connected, Disconnected, LoggedOut, HistorySync,
GroupUpdate, ContactUpdate, and Call. Call events are notifications only.
Historical messages are stored before HistorySync and never replayed as
NewMessage. Acknowledged local sends also produce outgoing NewMessage events.
Handlers are async functions, dispatched in registration order. Handler failures
are logged and do not stop other handlers. Connection/storage failures stop the
client and surface through the lifecycle waits. Keep handlers short: queues are
bounded and an EventOverflowError stops an overloaded client instead of silently
dropping events or growing memory indefinitely. Resume logic should reconcile
the local archive; this is not an exactly-once durable job queue.
client.remove_event_handler(handler) removes a registered function.
event.message and send results expose id, text, sender, chat,
timestamp (UTC), is_group, is_private, is_from_me, media,
quoted_message, mentions, sender_name, is_edit, and expires_at.
sender_alt and chat_alt retain alternate addresses only when WhatsApp supplies
them. Phone filters/history queries can match these known aliases; a LID alone
does not establish a phone-number association.
event.text, event.sender, and event.chat are convenient shortcuts.
sent = await client.send_message("+919876543210", "Hello!")
await sent.react("π")
edited = await sent.edit("Hello again!")
await edited.delete() # revoke for everyone, subject to WhatsApp rules
await client.send_message(group_id, "Hi @919876543210", mentions=["+919876543210"])
await client.send_location("+919876543210", 12.9716, 77.5946, name="Meeting point")
await client.send_contact("+919876543210", "Alice", "+14155550100")
await client.send_poll(group_id, "Lunch?", ["12:00", "13:00"], selectable_count=1)Reply with await event.reply(text) or
await client.send_message(entity, text, reply_to=message).
Numbers need a country code; no region is guessed. Bare/device JIDs and group, LID, newsletter, and broadcast IDs normalize internally. LIDs remain privacy identifiers and are never fabricated into phone numbers. Sending is supported to private chats and groups. Newsletter/status/broadcast sending and management are deliberately not exposed in this release.
resolve_entity("DevsDoCode") looks up an exact known contact/chat/group name.
Ambiguous names raise EntityNotFoundError; numbers and JIDs resolve directly.
from whatsapp_bridge.types import MediaType
await client.send_file("+919876543210", "photo.jpg", caption="Look at this")
await client.send_file("+919876543210", b"report data", filename="report.txt")
await client.send_file("+919876543210", "voice.ogg", voice_note=True)
await client.send_file("+919876543210", "sticker.webp", media_type=MediaType.STICKER)
path = await event.download_media() # returns pathlib.PathAccepts paths, bytes, and binary streams. Supply filename/mime_type for bytes
when inference is insufficient; media_type=MediaType.DOCUMENT forces attachment
delivery. Caller-owned streams stay open. Downloads use safe unique filenames;
an explicit file path must not already exist. Returned files belong to the caller
and are not temporary files deleted behind their back.
Image/video/document captions, audio, Ogg Opus voice notes, WebP stickers, and
MP4 GIF playback are supported. Supply duration for audio/video metadata when
known. The package sends pre-encoded files; it does not run FFmpeg, transcode
arbitrary GIFs, or generate previews. View-once payloads are not archived or
made downloadable. Expired messages are excluded from archive queries.
messages = await client.get_messages("+919876543210", limit=100, search="meeting")
async for message in client.iter_messages("+919876543210", limit=1000):
print(message.timestamp, message.text)
chats = await client.get_chats()
contacts = await client.get_contacts()
contact = await client.get_contact("+919876543210")History is the local synchronized archive, newest first. WhatsApp decides
what reaches a linked device; this API cannot retrieve arbitrary old messages
from the phone/server. It stores observed and synced messages in its own schema,
never polls or reads the native identity database. The default retention is
10,000 messages per session across all chats. Set history_limit=0 to disable
message retention. Expired records are hidden immediately by query predicates
and physically pruned during archive writes.
Bounded deletion markers prevent recent revocations being restored by a later
history batch. They contain chat/message IDs and timestamps, not message contents.
Filters: limit, exclusive UTC-aware before/after, search (case-insensitive
SQLite substring matching), sender, and media_type. Pagination preserves
messages sharing a timestamp. A disconnected client must be reconnected to
use its archive through this API.
Chats combine synchronized/observed chats with joined groups. Chat exposes
id, name, last_message, is_group, and is_private. Unread counts are not
exposed because the library cannot maintain a reliably authoritative count.
groups = await client.get_groups()
group = await client.get_group(group_id)
created = await client.create_group("Project", ["+919876543210"])
results = await client.update_participants(group_id, ["+919876543210"], "add")
await client.set_group_name(group_id, "New subject")
await client.set_group_description(group_id, "Project notes")
await client.set_group_photo(group_id, "photo.jpg")
link = await client.get_invite_link(group_id)
await client.join_group(link)
await client.leave_group(group_id)
await client.set_presence(online=True)
await client.subscribe_presence("+919876543210")
await client.set_typing(group_id, "composing") # also recording / paused
await event.message.mark_read()Participant actions are add, remove, promote, and demote. Inspect each
returned participant's error_code: a group operation can partially succeed.
Permission failures use errors.PermissionError. WhatsApp's privacy/consent and
admin rules still apply. Presence events require subscriptions; typing events
usually require the linked account to be available. get_profile_picture()
returns picture metadata/URL without fetching another site automatically.
from whatsapp_bridge import ClientConfig, WhatsAppClient
async def show_qr(qr):
# qr.text: sensitive pairing text; qr.png(): image bytes; qr.terminal(): text
await my_application.display_qr(qr.png())
client = WhatsAppClient(
"personal", qr_callback=show_qr, config=ClientConfig(history_limit=5000, reconnect_timeout=120)
)Defaults: Windows %LOCALAPPDATA%/whatsapp-bridge, macOS
~/Library/Application Support/whatsapp-bridge, Linux
${XDG_DATA_HOME:-~/.local/share}/whatsapp-bridge. Override with session_dir.
Each named directory has an identity database, archive, and OS-backed lease.
Different names are independent; concurrent reuse of one name fails. There is
no import-time filesystem mutation or shared global configuration.
Archives are bound to the authenticated account's phone JID. Linking a different account to
an existing named archive fails instead of mixing private histories; use another
name and unlink any unused device in WhatsApp's Linked Devices screen.
Use a new name after changing the account's phone number as well.
Session files and QR codes are credentials. Protect them and their backups.
POSIX session directories/files use 0700/0600. Windows uses the user's inherited
directory ACL; store sessions in a private user directory. Databases are local
and not encrypted at rest. Nothing is sent to an LLM, analytics service, or
third-party bridge. Corrupt sessions produce SessionError and are never
silently deleted. Logout keeps local history; remove a closed session directory
yourself if you also want to erase its archive.
ClientConfig also sets connect_timeout, request_timeout, shutdown_timeout,
event_queue_size, and max_media_bytes (default 64 MiB). Native Go requests use
deadlines; shutdown cancels their context and waits for ownership to end.
shutdown_timeout bounds terminal logout handlers. Other handlers must cooperate
with asyncio cancellation.
from whatsapp_bridge.errors import WhatsAppBridgeError
try:
await client.send_message("+919876543210", "Hello")
except WhatsAppBridgeError:
# Decide whether to report, wait, or reconcile an ambiguous send result.
raiseThe exception hierarchy includes authentication, connection, session, entity,
send, media, permission, rate-limit, backend, unsupported-feature, and overload
errors. Configure Python logging in your application; the package adds no root
handlers. QR display is the deliberate terminal-output exception.
There is no call answering/recording, newsletter management, arbitrary historical fetch, link-preview scraping, communities manager, or status publishing API. These are not simulated with unsupported operations. Live-account verification is separate from ordinary tests. See validation, examples, and the 0.1 migration guide.
See CONTRIBUTING.md for native builds, tests, lint, typing, and release wheels. The project is licensed under MPL-2.0; bundled components are recorded in third-party notices.