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.
hatang 0.1.1 · Python 3.9+ · PyPI · 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), có scope cho những gì bạn gọi:crm:writecho gói và giao dịch mua,chat:writecho hội thoại và kiến thức,email:sendcho email. - Với identity hash cho chat: identity secret của site (Hỗ trợ → Kênh → site → Widget).
Cài đặt
pip install hatang
# hoặc
uv add hatang
# hoặc
poetry add hatang
Tạo client
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):
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
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:
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
Đư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.
# 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.
Kiến thức
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.
Hội thoại và khách hàng
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
from là từ khoá của Python, nên viết body dưới dạng dict:
hatang.emails.send({
"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
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
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
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.
Kiểu dữ liệu
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
- Knowledge API
- REST API cho những gì SDK chưa bọc