Send WhatsApp Messages from Python
You can send WhatsApp messages from Python today with plain HTTP. The WhaBuzz API takes Meta's own /messages body and a Bearer key, so requests or httpx is enough. A Python SDK is documented but not yet published, so this post uses no WhaBuzz package. It covers sends, errors, idempotency and webhook signatures with hmac.
Is there a Python SDK for WhaBuzz?
Not yet. The Python SDK page describes a whabuzz package for Python 3.9+, but the package is coming and not published. Do not run pip install whabuzz and expect it to work. The Node.js package is live on npm; the Python one is not.
That is fine, because the API is small. Per the sending messages guide, WhaBuzz does not have its own message format. You post Meta's body with one extra field, from. Message bodies are Meta's own either way.
Install one HTTP library:
pip install requests
export WHABUZZ_API_KEY="wb_live_..."bashHow do you send a text message with Python?
Post JSON to https://api.whabuzz.com/v1/messages with an Authorization: Bearer header. The from field is the phone_number_id of one of your numbers, or its E.164 number.
import os
import requests
API = "https://api.whabuzz.com"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['WHABUZZ_API_KEY']}"
def send(body: dict, idempotency_key: str | None = None) -> dict:
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
r = session.post(f"{API}/v1/messages", json=body, headers=headers, timeout=30)
data = r.json()
if not r.ok:
raise WhaBuzzAPIError(r.status_code, data)
return data
res = send({
"from": "1013622581841046",
"messaging_product": "whatsapp",
"to": "16505551234",
"type": "text",
"text": {"body": "Your order has shipped."},
}, idempotency_key="order-48213-shipped")
print(res["messages"][0]["id"])pythonThe response is Meta's, with Meta's HTTP status. The documented success shape is {"messaging_product":"whatsapp","contacts":[...],"messages":[{"id":"wamid.HBgL…","message_status":"accepted"}]}. Keep the wamid: delivery, read and failure arrive later as message.status events matched on it.
Free-form text needs an open 24-hour customer window. WhaBuzz does not check the window or the content. Meta decides.
How do you send a template message?
Change type to template and pass Meta's template object. The shape below is the one in the getting started guide, and renewal_reminder is that guide's example name. Use a template that is approved on your account. See the templates guide.
res = send({
"from": "1013622581841046",
"messaging_product": "whatsapp",
"to": "16505551234",
"type": "template",
"template": {
"name": "renewal_reminder",
"language": {"code": "en"},
"components": [{
"type": "body",
"parameters": [
{"type": "text", "text": "Kerry"},
{"type": "text", "text": "31 Oct"},
],
}],
},
}, idempotency_key="renewal-48213-2026-10")pythonOne documented edge: a busy number can answer 202 {"status":"queued"}, and the message then goes out in order. Check for messages in the response before indexing into it.
How do you make retries safe?
Send an Idempotency-Key header, as the code above does. According to the idempotency guide, the value can be up to 255 characters, the first answer is stored for 24 hours, and a repeat with the same key and body replays that answer with Idempotent-Replayed: true without sending to Meta. A key reused with a different body gives 422 idempotency_conflict; a key whose original is still running gives 409 idempotency_conflict.
With requests, a timeout raises an exception, and you do not know whether the message went out. Retry with the same key:
import time
def send_with_retry(body, key, attempts=3):
for i in range(attempts):
try:
return send(body, idempotency_key=key)
except (requests.Timeout, requests.ConnectionError):
time.sleep(2 ** i)
raise RuntimeError("no answer; check message.status events before using a new key")pythonFor the reasoning behind key design and the 502 meta_unreachable case, read Stop duplicate WhatsApp API messages.
How do you handle errors in Python?
Every error response says who produced it. The errors page defines a source field: whabuzz when WhaBuzz stopped the request, meta when Meta answered with an error, which comes back verbatim with Meta's own HTTP status. Every response carries a request_id.
class WhaBuzzAPIError(Exception):
def __init__(self, status, body):
self.status = status
self.source = body.get("source")
self.error = body.get("error", {})
self.request_id = body.get("request_id")
super().__init__(f"{status} {self.source} {self.error.get('code')}")
try:
send(body)
except WhaBuzzAPIError as e:
if e.source == "meta":
print("Meta refused", e.error.get("code"), e.error.get("message"))
else:
print("WhaBuzz", e.error.get("code"), e.request_id)pythonLog request_id; you can look it up with GET /v1/requests/{request_id}. For Meta errors, branch on error.code and keep error.fbtrace_id. Do not auto-retry Meta 4xx responses, per the errors page. The WhaBuzz codes include number_not_connected (409) and invalid_request (400). Use .get so a missing field never raises a second error.
How do you verify a webhook signature with hmac?
WhaBuzz signs every delivery. The webhooks guide says X-WhaBuzz-Signature looks like t=1760591523,v1=5b0c…e1, where v1 is the lowercase hex HMAC-SHA256, keyed with your signing secret, of the string {t}.{raw request body}. During a secret rotation the header carries two v1= values, and you accept the request if any matches. Reject it if t is more than 300 seconds from your clock.
This is the Python function from that guide:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.split("=", 1) for p in header.split(",")]
t = int(next(v for k, v in parts if k == "t"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")pythonUse it on the raw bytes, before any JSON parsing. In Flask that is request.get_data(), not request.json. Return a 2xx quickly, store the event, and dedupe on dedupe_key. The full pattern is in WhatsApp webhooks that never lose an event.
When should you use httpx instead of requests?
Use httpx when your app is async, for example FastAPI or a worker that sends many messages at once. The request is the same: a Bearer header, Meta's body and an Idempotency-Key. Only the client changes.
import httpx
async def send_async(client: httpx.AsyncClient, body: dict, key: str) -> dict:
r = await client.post(
"https://api.whabuzz.com/v1/messages",
json=body,
headers={"Idempotency-Key": key},
timeout=30,
)
data = r.json()
if r.is_error:
raise WhaBuzzAPIError(r.status_code, data)
return datapythonCreate the client once with httpx.AsyncClient(headers={"Authorization": f"Bearer {key}"}) and reuse it. Do not send two concurrent requests with the same idempotency key; the guide says not to, and the second gets a 409.
For thousands of messages, do not loop over single sends. POST /v1/messages/batch queues 1 to 10,000 messages in one call and answers 202, per the messages guide. Pace and limits are in the rate limits guide.
Frequently asked questions
Can I pip install whabuzz today?
No. The Python SDK is documented but not published yet. Use plain HTTP, as shown here.
Do I need requests, or does httpx work?
Either works. The API is ordinary HTTPS with JSON bodies and a Bearer key.
Does the Python SDK change the message format?
The documented SDK passes Meta's bodies through, the same as the API. Your message dicts should carry over.
How long should my request timeout be?
Per the errors page, WhaBuzz answers 502 meta_unreachable if the Graph API does not answer within 15 seconds. A client timeout of 30 seconds leaves room for that answer.
What if my signature check always fails?
Check you are hashing the raw bytes, not re-serialised JSON, and that you use the right endpoint's signing secret.
Related reading
- Python SDK page and the sending messages guide.
- Stop duplicate WhatsApp API messages.
- Send WhatsApp messages from Node.js for the published SDK.
- All guides by task and the WhatsApp API overview.