Metadata-Version: 2.4
Name: whatsarab
Version: 0.1.0
Summary: Official Python client for the WhatsArab API — WhatsApp messages, media, templates, OTP, and signed webhooks on the official Meta Cloud API.
Project-URL: Homepage, https://whatsarab.com/docs
Project-URL: Documentation, https://whatsarab.com/docs
Project-URL: Source, https://github.com/efayek/whatsapp-api/tree/main/sdks/python
Project-URL: Support, https://whatsarab.com/support
Author: WhatsArab
License: MIT
Keywords: arabic,mena,messaging,meta-cloud-api,otp,whatsapp,whatsapp-business-api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# whatsarab

Official Python client for the [WhatsArab](https://whatsarab.com) API — send WhatsApp messages on the official Meta Cloud API, upload media, manage templates, send and verify one-time codes, and verify signed webhooks.

```bash
pip install whatsarab
```

Python 3.9+.

## Quick start

```python
import os
from whatsarab import WhatsArab

wa = WhatsArab(api_key=os.environ["WHATSARAB_API_KEY"])

# Free-form text — only inside the 24-hour customer-service window
wa.messages.send(to="201234567890", text="أهلًا يا سارة! طلبك رقم ١٠٤٢٨ خرج للتوصيل.")

# An approved template — works any time
wa.messages.send(
    to="201234567890",
    template="order_shipped",
    language="ar",
    components=[{"type": "body", "parameters": [{"type": "text", "text": "10428"}]}],
)
```

Get an API key from **/app/integrations → API keys**. Keys are shown once, at creation.

## What this covers

The API-key surface, which is the eleven operations under `/api` in the [OpenAPI document](https://whatsarab.com/public/openapi.json):

| | |
|---|---|
| `wa.messages.send()` | text · template · interactive · media |
| `wa.media.upload()` / `.download()` | multipart up, raw bytes back |
| `wa.templates.list() / .create() / .edit() / .delete()` | Meta template management |
| `wa.otp.send()` / `.verify()` | one-time codes over WhatsApp |
| `wa.events.list()` | recent webhook events |
| `verify_webhook_signature()` | HMAC check for deliveries to you |

**Not covered, deliberately:** contacts, the inbox and broadcasts are session-authenticated — they belong to a signed-in dashboard user, not to a server-side key — so there is no endpoint for this client to call. If that changes, it changes in the OpenAPI document first.

## Sending

Exactly one of `template`, `text`, `interactive` or `media` per call — checked locally, so a mistake is a `ValueError` on your machine rather than a 400 from production.

```python
# Media: upload once, then send by id
with open("invoice.pdf", "rb") as fh:
    uploaded = wa.media.upload(fh, filename="invoice.pdf", content_type="application/pdf")

wa.messages.send(
    to="201234567890",
    media={"id": uploaded["media_id"], "kind": "document", "filename": "invoice.pdf"},
)

# Interactive buttons
wa.messages.send(
    to="201234567890",
    interactive={
        "type": "button",
        "body": {"text": "تحب نأكد الطلب؟"},
        "action": {"buttons": [{"type": "reply", "reply": {"id": "yes", "title": "أكيد"}}]},
    },
)

# Multi-number workspace: pick the sender explicitly
wa.messages.send(to="201234567890", text="hi", from_phone_number_id="113456789012345")

# Check the call without spending a message
wa.messages.send(to="201234567890", template="order_shipped", dry_run=True)
```

`text`, `interactive` and `media` are free but only deliverable inside the recipient's 24-hour customer-service window. Outside it the call fails with `OUTSIDE_CSW` and you need an approved template.

The response is a `202`: Meta accepted the message. Delivery arrives later on the webhook bus, matched by `message_id` (the wamid).

## One-time codes

```python
sent = wa.otp.send(to="201234567890", template="otp_login", language="ar")
result = wa.otp.verify(request_id=sent["request_id"], code="123456")

if result["verified"]:
    ...
```

The code is generated, hashed and checked on our side — it is never returned to you.

## Errors

Every failure — including a refused connection or a timeout — is a `WhatsArabError`, so one `except` covers the lot. Branch on `.code`, which is stable; the message is for humans and may be reworded.

```python
from whatsarab import WhatsArabError

try:
    wa.messages.send(to="201234567890", text="hi")
except WhatsArabError as err:
    if err.code == "OUTSIDE_CSW":
        send_template_instead()
    elif err.code == "OVER_QUOTA":
        queue_for_next_month()
    elif err.code in ("TIMEOUT", "NETWORK_ERROR"):
        retry_later()
    else:
        raise
```

`err.request_id` is worth logging — quote it in a support ticket and we can find the exact request.

## Verifying webhooks

Deliveries to your endpoint carry `X-Whatsarab-Timestamp` and `X-Whatsarab-Signature: sha256=<hex>`, computed over `f"{timestamp}.{raw_body}"` with your subscription secret.

```python
from fastapi import FastAPI, HTTPException, Request
from whatsarab import verify_webhook_signature

app = FastAPI()

@app.post("/webhooks/whatsarab")
async def hook(request: Request):
    body = await request.body()          # the raw bytes
    ok = verify_webhook_signature(
        secret=os.environ["WHATSARAB_WEBHOOK_SECRET"],
        raw_body=body,
        signature_header=request.headers.get("x-whatsarab-signature"),
        timestamp_header=request.headers.get("x-whatsarab-timestamp"),
    )
    if not ok:
        raise HTTPException(status_code=401)
    ...
```

**Pass the raw body.** A body that has been parsed and re-serialized will not match — that is the single commonest way this check is got wrong. The comparison is constant-time, and deliveries more than five minutes old are rejected (`tolerance_seconds=0` turns that off).

## Options

```python
WhatsArab(
    api_key="…",                       # required
    base_url="https://whatsarab.com",  # override for staging or self-hosted
    timeout=30.0,                      # seconds; None disables
)
```

The client holds an `httpx.Client`. Close it when you are done, or use it as a context manager:

```python
with WhatsArab(api_key="…") as wa:
    wa.messages.send(to="201234567890", text="hi")
```

## Links

- [Docs](https://whatsarab.com/docs) · [OpenAPI](https://whatsarab.com/public/openapi.json) · [Guides](https://whatsarab.com/how-to) · [Support](https://whatsarab.com/support)

MIT.
