# Send WhatsApp Messages from Python

> Send WhatsApp template and text messages from Python with plain requests against the WhaBuzz REST API, handle errors and verify webhooks with hmac.

Source: https://whabuzz.com/blog/whatsapp-api-python/
By Smaran Challapalli. Published 16 Oct 2026. Updated 16 Oct 2026.

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](https://docs.whabuzz.com/sdks/python/) 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](https://docs.whabuzz.com/guides/messages/), 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:

```bash
pip install requests
export WHABUZZ_API_KEY="wb_live_..."
```

## How 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.

```python
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"])
```

The 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](https://docs.whabuzz.com/getting-started/), and `renewal_reminder` is that guide's example name. Use a template that is approved on your account. See the [templates guide](https://docs.whabuzz.com/guides/templates/).

```python
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")
```

One 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](https://docs.whabuzz.com/guides/idempotency/), 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:

```python
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")
```

For the reasoning behind key design and the `502 meta_unreachable` case, read [Stop duplicate WhatsApp API messages](https://whabuzz.com/blog/whatsapp-api-idempotency/).

## How do you handle errors in Python?

Every error response says who produced it. The [errors page](https://docs.whabuzz.com/errors/) 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`.

```python
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)
```

Log `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](https://docs.whabuzz.com/guides/webhooks/) 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:

```python
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")
```

Use 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](https://whabuzz.com/blog/whatsapp-webhook-reliability/).

## 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.

```python
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 data
```

Create 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](https://docs.whabuzz.com/guides/rate-limits/).

## 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](https://docs.whabuzz.com/sdks/python/) and the [sending messages guide](https://docs.whabuzz.com/guides/messages/).
- [Stop duplicate WhatsApp API messages](https://whabuzz.com/blog/whatsapp-api-idempotency/).
- [Send WhatsApp messages from Node.js](https://whabuzz.com/blog/whatsapp-api-nodejs/) for the published SDK.
- [All guides by task](https://whabuzz.com/guides/) and the [WhatsApp API overview](https://whabuzz.com/whatsapp-api/).
