WhatsApp Client
Async Python client for the WhatsApp Business Cloud API.
[!NOTE] This package was mainly LLM-generated (Claude), with very strong opinions from @Kludex.
Requires Python 3.10+
Installation
Install with uv:
uv add whatsapp-client
Setup
You need two credentials from Meta to use this package:
1. Phone Number ID
- Go to the Meta Developer Portal and create a Business app.
- In your app dashboard, add the WhatsApp product.
- Go to WhatsApp > API Setup — your Phone Number ID is listed below the test phone number.
2. Access Token
For testing, click Generate on the API Setup page to get a temporary 24-hour token.
For production, create a System User in Meta Business Suite:
- Create a System User with Admin role.
- Assign your WhatsApp app to the System User.
- Generate a token with the
whatsapp_business_messagingpermission.
3. Add test recipients
In test mode, you must verify recipient phone numbers before sending messages. Go to WhatsApp > API Setup, click Manage phone number list, and add the numbers you want to message.
Usage
Send a text message:
from whatsapp_client import WhatsAppClient
async with WhatsAppClient(phone_number_id="your-phone-number-id", access_token="your-access-token") as client:
await client.send_text(to="5511999999999", body="Hello!")
Send media
Images, audio, video, documents, and stickers are sent by URL:
await client.send_image(to="5511999999999", link="https://example.com/image.png", caption="Check this out")
await client.send_audio(to="5511999999999", link="https://example.com/audio.mp3")
await client.send_video(to="5511999999999", link="https://example.com/video.mp4")
await client.send_document(to="5511999999999", link="https://example.com/doc.pdf", filename="report.pdf")
await client.send_sticker(to="5511999999999", link="https://example.com/sticker.webp")
Send location
Share a pin with optional name and address:
await client.send_location(to="5511999999999", latitude=-23.5505, longitude=-46.6333, name="São Paulo")
Send template
Send a pre-approved message template:
from whatsapp_client import Template, TemplateLanguage
await client.send_template(
to="5511999999999", template=Template(name="hello_world", language=TemplateLanguage(code="en_US"))
)
Send interactive messages
Reply buttons and list menus let users pick from predefined options:
from whatsapp_client import ReplyButton, ListSection, ListRow
await client.send_buttons(
to="5511999999999",
body="Choose an option:",
buttons=[ReplyButton(id="btn1", title="Option 1"), ReplyButton(id="btn2", title="Option 2")],
)
await client.send_list(
to="5511999999999",
body="Browse products:",
button_text="View",
sections=[ListSection(title="Category", rows=[ListRow(id="r1", title="Item 1")])],
)
Error handling
API errors are raised as WhatsAppAPIError with the status code and Graph API error details:
from whatsapp_client import WhatsAppAPIError
try:
await client.send_text(to="invalid", body="Hello")
except WhatsAppAPIError as e:
print(e.status_code, e.error_code, e.message)
Webhooks
Receive incoming messages and status updates via Meta's webhook system.
Starlette example
Create a WebhookHandler with your app secret and client. Register callbacks with @handler.on_message and @handler.on_status — the client is passed as the first argument, so replying is straightforward. Use match/case on message.content to handle different message types:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import PlainTextResponse, Response
from starlette.routing import Route
from whatsapp_client import (
MediaContent,
Message,
Status,
TextContent,
WebhookHandler,
WebhookNotification,
WhatsAppClient,
verify_challenge,
)
client = WhatsAppClient(phone_number_id="your-phone-number-id", access_token="your-access-token")
handler = WebhookHandler(app_secret="your-app-secret", client=client)
@handler.on_message
async def on_message(client: WhatsAppClient, notification: WebhookNotification, message: Message) -> None:
match message.content:
case TextContent(body=body):
await client.send_text(to=message.from_, body=f"Echo: {body}")
case MediaContent():
await client.send_text(to=message.from_, body=f"Got {message.type}: {message.content.id}")
@handler.on_status
async def on_status(client: WhatsAppClient, notification: WebhookNotification, status: Status) -> None:
print(f"{status.id} -> {status.status}")
async def webhook_get(request: Request) -> Response:
challenge = verify_challenge(
mode=request.query_params["hub.mode"],
token=request.query_params["hub.verify_token"],
challenge=request.query_params["hub.challenge"],
verify_token="your-verify-token",
)
return PlainTextResponse(challenge)
async def webhook_post(request: Request) -> Response:
body = await request.body()
signature = request.headers["x-hub-signature-256"]
await handler.handle(body=body, signature=signature)
return Response(status_code=200)
@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncIterator[None]:
async with client:
yield
app = Starlette(
routes=[Route("/webhook", webhook_get, methods=["GET"]), Route("/webhook", webhook_post, methods=["POST"])],
lifespan=lifespan,
)
License
This project is licensed under the terms of the MIT License.
Metadata
Release files for whatsapp-client 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| whatsapp_client-0.2.0.tar.gz | 57.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| whatsapp_client-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 69.2 kB
Release files / whatsapp_client-0.2.0.tar.gz
| Download URL | whatsapp_client-0.2.0.tar.gz |
|---|---|
| Size | 57.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5a2319f8e8464382e031a7d6d5d2241cb18e9407090482485aca4a2e92a73d3b
|
|
BLAKE2b-256 checksum How to use checksums |
f78a5bb9ba884412a3479e0e4ca69df087e0f78af3eada80cdb3479b8f08a776
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 7, 2026.
Transparency logRelease files / whatsapp_client-0.2.0-py3-none-any.whl
| Download URL | whatsapp_client-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c278827562b39e861eeb73682594812ef71d2447d465d8455f78e1c069f67ec8
|
|
BLAKE2b-256 checksum How to use checksums |
89daf35d35a17e2e1d46932fc999f5a296984954bce2c1a1e3a42b8f0e8cc1fe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 7, 2026.
Transparency log