# SDK Python (`hatang`)

`hatang` là client phía server cho API Hạ Tầng bằng Python. Nó lo những việc backend hay làm nhất: đưa gói và giao dịch mua lên hồ sơ khách, đồng bộ kiến thức cho trợ lý AI, trả lời hội thoại, gửi email, tạo thanh toán VietQR, và xác minh webhook, IPN. Có client đồng bộ (`Hatang`) và client asyncio (`AsyncHatang`) với cùng các hàm.

> **Info: Phiên bản**
>
> `hatang` **0.1.1** · Python 3.9+ · [PyPI](https://pypi.org/project/hatang/) · changelog: `CHANGELOG.md` trong package

## Trước khi bắt đầu

- Một API key (`ht_...`), tạo trong dashboard (xem [Xác thực](https://hatang.dev/vi/docs/guides/getting-started/authentication.md)), có scope cho những gì bạn gọi: `crm:write` cho gói và giao dịch mua, `chat:write` cho hội thoại và kiến thức, `email:send` cho email.
- Với identity hash cho chat: identity secret của site (**Hỗ trợ → Kênh → site → Widget**).

## Cài đặt

```bash
pip install hatang
# hoặc
uv add hatang
# hoặc
poetry add hatang
```

## Tạo client {#client}

```python
from hatang import Hatang

hatang = Hatang()  # đọc HATANG_API_KEY (và HATANG_BASE_URL, HATANG_ENVIRONMENT)
hatang = Hatang("ht_...", base_url="http://localhost:3001", timeout=10)
```

Đóng client khi xong (`hatang.close()`, hoặc `with Hatang() as hatang:`), hoặc giữ một client suốt vòng đời tiến trình. Truyền `httpx.Client` của riêng bạn qua `http_client` khi cần proxy, retry hoặc test.

asyncio (FastAPI, aiohttp, Starlette):

```python
from hatang import AsyncHatang

hatang = AsyncHatang()

async def on_paid(order):
    await hatang.chat.purchases.upsert("web", order.id, {...})
```

## Kết quả và lỗi {#errors}

Mọi hàm trả về JSON của API dưới dạng `dict`. Gọi không thành công thì ném `HatangError`:

```python
from hatang import HatangError

try:
    hatang.chat.purchases.upsert("web", order.id, body)
except HatangError as e:
    e.status_code  # mã HTTP; 0 khi không tới được Hạ Tầng (mạng, timeout)
    e.name         # validation_error, not_found, rate_limit_exceeded, ...
    e.retryable    # True khi không tới được, 429 hoặc 5xx: nên thử lại sau
```

Đây là cách quen thuộc của Python; SDK Node trả về `{ data, error }`. Tên trường giữ nguyên snake_case của API ở cả hai.

## Gói và giao dịch mua {#purchases}

Đưa những gì khách đang trả tiền lên hồ sơ khách, để nhân viên và trợ lý AI trả lời "gói của tôi khi nào gia hạn?" bằng dữ liệu thật. Dùng đúng user id bạn truyền vào `identify` trong chat.

```python
# Gói bán trên website (hoặc gói bạn đã tự xác minh với App Store / Google Play).
hatang.chat.subscriptions.upsert("web", subscription.id, {
    "status": "active",
    "sku": "PRO-1M",                      # SKU của sản phẩm trong danh mục
    "external_user_id": str(user.id),
    "started_at": "2026-10-01T00:00:00Z",
    "current_period_end": "2026-11-01T00:00:00Z",
    "auto_renew": False,
    "amount": 99000, "currency": "VND",
})

# Mua một lần: gói xu, mở khoá, khoá học. Hoàn tiền thì gửi status="refunded" với cùng mã.
hatang.chat.purchases.upsert("web", order.id, {
    "store_product_id": "credits:300",    # mã riêng của bạn trên web; mã sản phẩm của cửa hàng trong app
    "sku": "XU-300",
    "kind": "consumable",
    "external_user_id": str(user.id),
    "amount": 29000, "currency": "VND",
    "purchased_at": "2026-10-30T10:00:00Z",
})
hatang.chat.purchases.list(external_user_id=str(user.id))
```

Giao dịch trong cửa hàng dùng platform `ios` (originalTransactionId của Apple cho gói, transactionId cho mua một lần) hoặc `android` (purchase token, hoặc order id của Play). Gửi lại cùng một bản ghi chỉ cập nhật nó, nên backend có thể gửi lại mọi thứ nó biết sau mỗi thay đổi. Xem [Mua trong ứng dụng và gói thuê bao](https://hatang.dev/vi/docs/guides/support/in-app-purchases.md#backend-only).

## Kiến thức {#knowledge}

```python
hatang.knowledge.sync("help", [
    {"external_id": f"help:{a.slug}", "title": a.title, "content": a.body, "url": a.url, "locale": "vi"}
    for a in articles
])  # bộ sưu tập chỉ còn đúng các tài liệu này; tài liệu không đổi thì không lập chỉ mục lại
hatang.knowledge.search("đổi trả trong bao lâu", locale="vi")
```

Xem [Knowledge API](https://hatang.dev/vi/docs/guides/support/knowledge-api.md).

## Hội thoại và khách hàng {#chat}

```python
convs = hatang.chat.conversations.list(status="open", unanswered=True)
hatang.chat.conversations.send(conv_id, {"content": "Đơn của bạn đã được gửi."}, idempotency_key=f"shipped:{order.id}")
hatang.chat.conversations.send(conv_id, {"content": "Khách VIP", "visibility": "private"})  # ghi chú nội bộ
hatang.chat.customers.update(customer_id, {"stage": "won", "fields": {"plan": "pro"}})
csv_text = hatang.chat.customers.export_csv({"stage": "won"})
```

Sản phẩm và đơn hàng: `hatang.chat.products` (`list`, `search`, `get`, `create`, `update`, `remove`) và `hatang.chat.orders` (`list`, `get`, `create`, `update`, `payment_link`).

## Email {#emails}

`from` là từ khoá của Python, nên viết body dưới dạng dict:

```python
hatang.emails.send({
    "from": "Shop <orders@shop.vn>",
    "to": "an@example.com",
    "subject": "Đơn hàng DH12",
    "html": "<p>Cảm ơn bạn!</p>",
    "attachments": [{"filename": "invoice.pdf", "content": pdf_bytes}],  # bytes được mã hoá base64 giúp bạn
}, idempotency_key=f"order-mail:{order.id}")
hatang.batch.send([...])  # tối đa 100
```

## Thanh toán và IPN {#payments}

```python
payment = hatang.payments.create({"amount": 199000, "description": "DH12", "ipn_url": "https://shop.vn/hatang/ipn"})

from hatang import construct_ipn_event

def ipn(request):  # Django; truyền body thô trước khi parse
    event = construct_ipn_event(request.body, request.headers, settings.HATANG_SECRET_KEY)
    if event["data"]["status"] == "paid":
        mark_paid(event["data"]["metadata"]["order_id"])
```

## Webhook {#webhooks}

```python
from hatang import HatangSignatureError, construct_webhook_event

@app.post("/hatang/webhook")          # FastAPI
async def webhook(request: Request):
    try:
        event = construct_webhook_event(await request.body(), request.headers, WEBHOOK_SECRET)  # whsec_...
    except HatangSignatureError:
        raise HTTPException(400)
    if event["type"] == "chat.message.created":
        ...
```

Lần gửi cũ hơn 5 phút bị từ chối (đổi bằng `tolerance_sec`). `verify_webhook` và `verify_ipn` trả về bool thay vì ném lỗi.

## Identity hash và purchase token {#identity-hash}

```python
from hatang import identity_hash, purchase_token

identity_hash(str(user.id), settings.HATANG_IDENTITY_SECRET)  # trả {userId, hash} cho widget hoặc app
purchase_token("site_...", str(user.id))  # StoreKit appAccountToken / Play obfuscatedAccountId
```

Cả hai cho kết quả giống hệt server Hạ Tầng và các SDK app. Xem [Xác minh danh tính](https://hatang.dev/vi/docs/guides/support/web-widget/identity-verification.md).

## Kiểu dữ liệu {#types}

Body của request là các `TypedDict` trong `hatang.types` (`ChatSubscriptionOptions`, `ChatPurchaseOptions`, `KnowledgeDocumentInput`, `CreateEmailOptions`, `CreatePaymentOptions`…), nên mypy và editor kiểm tra được tên trường. Package có kèm `py.typed`.

## Tiếp theo

- [Mua trong ứng dụng và gói thuê bao](https://hatang.dev/vi/docs/guides/support/in-app-purchases.md)
- [Knowledge API](https://hatang.dev/vi/docs/guides/support/knowledge-api.md)
- [REST API](https://hatang.dev/vi/docs/reference/api.md) cho những gì SDK chưa bọc

Source: https://hatang.dev/vi/docs/guides/sdks/python
