Chuyển tới nội dung chính

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.

Phiên bản

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: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​

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({
"from": "Shop <[email protected]>",
"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​

Xem trang này dạng Markdown