Metadata-Version: 2.4
Name: waotomatis
Version: 0.4.0
Summary: Official Python SDK for WAOtomatis — headless WhatsApp (WABA Cloud API).
Project-URL: Homepage, https://waotomatis.com
Project-URL: Documentation, https://docs.waotomatis.com
Project-URL: Source, https://github.com/waotomatis/waotomatis
Author: WAOtomatis
License: MIT
License-File: LICENSE
Keywords: sdk,waba,waotomatis,whatsapp,whatsapp-api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# waotomatis

Official Python SDK for [WAOtomatis](https://waotomatis.com) — headless WhatsApp
infrastructure on the WhatsApp Business Platform (WABA Cloud API). Send messages,
upload media, list chats and contacts, register webhooks, and verify webhook
signatures.

- **Zero dependencies.** Pure standard library (`urllib`, `hmac`, `hashlib`).
- **Python 3.8+.** Type-hinted, `py.typed`.
- **Idiomatic.** snake_case methods, keyword arguments, a typed error hierarchy.

## Install

```bash
pip install waotomatis
```

## Quickstart

```python
import os
from waotomatis import Waotomatis

wao = Waotomatis(api_key=os.environ["WAO_API_KEY"])

msg = wao.sessions("sess_123").messages.send_text(
    to="628123456789",
    text="Halo dari WAOtomatis 👋",
)

print(msg["id"])  # msg_abc123
```

`Waotomatis` defaults to `https://api.waotomatis.com`; pass `base_url=...` to
override. The API key is sent as `Authorization: Bearer <api_key>`.

## Sending messages

There are eight send methods — one per message type. Each is keyword-only,
returns `{"id", "eventId", "providerMessageId", "status"}`, and takes an optional
`reply_to` (quote a prior `wamid`) and `idempotency_key` (safe-retry key).

```python
session = wao.sessions("sess_123")

# 1. Text (optionally with a link preview)
session.messages.send_text(to="628...", text="Hi", preview_url=True)

# 2. Media — type is image | video | audio | document | sticker.
#    Provide a media_id (from media.upload) OR a public link.
session.messages.send_media(to="628...", type="image", link="https://example.com/a.jpg",
                            caption="Hello")
session.messages.send_media(to="628...", type="document", media_id="media_abc",
                            file_name="invoice.pdf")
session.messages.send_media(to="628...", type="audio", media_id="media_abc", voice=True)

# 3. Template (approved template by name + language)
session.messages.send_template(to="628...", name="order_update", language_code="en_US",
                               components=[{"type": "body",
                                            "parameters": [{"type": "text", "text": "42"}]}])

# 4. Interactive — type is button | list | cta_url | flow | product | product_list.
session.messages.send_interactive(
    to="628...", type="button", body_text="Confirm your order?",
    buttons=[{"id": "yes", "title": "Yes"}, {"id": "no", "title": "No"}],
    footer_text="WAOtomatis",
)
session.messages.send_interactive(
    to="628...", type="list", body_text="Pick a service", list_button="View menu",
    sections=[{"title": "Services", "rows": [
        {"id": "svc_1", "title": "Consultation", "description": "30 min"},
        {"id": "svc_2", "title": "Repair"},
    ]}],
)
session.messages.send_interactive(
    to="628...", type="cta_url", body_text="See our full catalog",
    cta_display_text="Open catalog", cta_url="https://example.com/catalog",
)
# flow / product / product_list ride the same method:
#   type="flow"          → flow={"flowCta": "Start", "flowId": "123"}
#   type="product"       → catalog_id=..., product_retailer_id=...
#   type="product_list"  → catalog_id=..., product_sections=[...]

# 5. Reaction (emoji="" clears it; reactions have no reply_to)
session.messages.send_reaction(to="628...", message_id="wamid.HBg...", emoji="👍")

# 6. Location
session.messages.send_location(to="628...", latitude=-6.2, longitude=106.8,
                               name="HQ", address="Jakarta")

# 7. Contacts (one or more vCards)
session.messages.send_contacts(to="628...", contacts=[
    {"name": {"formatted_name": "Ada Lovelace"},
     "phones": [{"phone": "+628111", "type": "WORK"}]},
])

# 8. Carousel template
session.messages.send_carousel(
    to="628...", name="summer_promo", language_code="en_US", body_params=["20%"],
    cards=[{"headerImageLink": "https://example.com/1.jpg", "bodyParams": ["Shoes"],
            "buttons": [{"subType": "quick_reply", "index": 0, "payload": "buy_1"}]}],
)

# Idempotent send (safe to retry — the same key returns the original result)
session.messages.send_text(to="628...", text="hi", idempotency_key="order-42")

# Mark an inbound message read by its provider wamid
session.messages.mark_read("wamid.HBg...")
```

## Media

```python
session = wao.sessions("sess_123")

# Upload raw bytes
with open("photo.jpg", "rb") as f:
    res = session.media.upload(f.read(), file_name="photo.jpg", mime_type="image/jpeg")
print(res["mediaId"])

# Or upload a local file by path
res = session.media.upload_file("photo.jpg", mime_type="image/jpeg")

# Or upload by URL
res = session.media.upload_from_url("https://example.com/photo.jpg")

# Download inbound media bytes
data, mime_type = session.media.download("media_abc")
```

## Sessions, chats, and contacts

```python
# List sessions (one page)
page = wao.list_sessions()
for s in page:
    print(s["id"], s["status"])

session = wao.sessions("sess_123")
session.get()
session.delete()  # disconnect

# Chats and contacts auto-paginate — iterate every item across all pages
for chat in session.chats.list():
    print(chat["chatId"], chat.get("lastText"))

for message in session.chats.history("628123456789"):
    print(message["direction"], message["type"])

for contact in session.contacts.list():
    print(contact["waId"], contact.get("name"))

# Or grab just one page
first = session.contacts.list(limit=50).first_page()
print(len(first), first.has_more, first.cursor)

contact = session.contacts.get("628123456789")
```

## Webhooks

Register a webhook — the signing `secret` is returned **once**:

```python
hook = wao.sessions("sess_123").webhooks.create(
    url="https://example.com/webhook",
    events=["message.received", "message.updated", "session.status"],
)
secret = hook["secret"]  # store this
```

Verify and parse incoming deliveries. The server signs the **exact raw request
body** with HMAC-SHA256 and sends it in the `X-Wao-Signature: sha256=<hex>`
header. Verify against the raw bytes you received — never a re-serialized object:

```python
from waotomatis import construct_event, verify_webhook, WaotomatisError

# Just verify
ok = verify_webhook(raw_body, request.headers.get("X-Wao-Signature"), secret)

# Verify + parse (raises on a bad signature or unparseable body)
try:
    event = construct_event(raw_body, request.headers.get("X-Wao-Signature"), secret)
except WaotomatisError:
    return ("", 401)

if event["event"] == "message.received":
    print(event["data"]["text"])
```

### Flask example

```python
import os
from flask import Flask, request, abort
from waotomatis import construct_event, WaotomatisError, WEBHOOK_SIGNATURE_HEADER

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["WAO_WEBHOOK_SECRET"]

@app.post("/webhook")
def webhook():
    try:
        event = construct_event(
            request.get_data(),  # raw bytes
            request.headers.get(WEBHOOK_SIGNATURE_HEADER),
            WEBHOOK_SECRET,
        )
    except WaotomatisError:
        abort(401)
    # handle event...
    return ("", 200)
```

## Templates

Manage the WhatsApp message templates registered on the session's connected
WABA. `components` is Meta's template-component array (`HEADER` / `BODY` /
`FOOTER` / `BUTTONS` objects) and is passed through as-is.

```python
session = wao.sessions("sess_123")

# List templates (single page; filterable by name/language/status/category)
page = session.templates.list(limit=50, category="UTILITY")
for tmpl in page:
    print(tmpl["name"], tmpl["language"], tmpl["status"])

# Fetch every language version of one template by name
for tmpl in session.templates.get("order_update"):
    print(tmpl["language"], tmpl["status"])

# Submit a new template for Meta approval
result = session.templates.create(
    name="order_update",
    language="en_US",
    category="UTILITY",
    components=[
        {"type": "BODY", "text": "Your order {{1}} has shipped."},
    ],
    allow_category_change=True,
)
print(result["id"], result["status"])

# Delete a template by name (all language versions)
session.templates.delete("order_update")
```

## Errors

Every failure raises a subclass of `WaotomatisError`, carrying the stable
`code`, `message`, `request_id`, and HTTP `status` from the server's uniform
error model (`{"error": {"code", "message", "requestId"}}`).

```python
from waotomatis import (
    Waotomatis, WaotomatisError,
    AuthenticationError, PermissionError, NotFoundError,
    ValidationError, RateLimitError, ApiError,
    ConnectionError, TimeoutError,
)
import time

try:
    wao.sessions("sess_123").messages.send_text(to="628...", text="hi")
except RateLimitError as e:
    time.sleep(e.retry_after or 1)
except WaotomatisError as e:
    if e.code == "session_disconnected":
        ...
    print(e.code, e.status, e.request_id)
```

| Exception            | HTTP        |
|----------------------|-------------|
| `AuthenticationError`| 401         |
| `PermissionError`    | 403         |
| `NotFoundError`      | 404         |
| `TimeoutError`       | 408 / local |
| `ValidationError`    | 409 / 422   |
| `RateLimitError`     | 429         |
| `ApiError`           | 5xx         |
| `ConnectionError`    | network     |

Transient failures (408/429/5xx/network) on idempotent verbs — or any call given
an `idempotency_key` — are retried automatically with exponential backoff and
jitter, honoring `Retry-After`. Tune with `max_retries=` and `timeout=` on the
constructor.

## License

MIT
