Giới thiệu chung
Hệ thống B2B API của Shop MMO Uy Tin cung cấp cơ chế bảo vệ giao dịch chống trừ tiền kép (double-spend), bảo vệ tính toàn vẹn tài chính cho đối tác qua khóa Idempotency và quy trình xử lý lỗi fail-closed.
- Kết nối trực tiếp: Lấy danh mục, kiểm tra tồn kho, đặt hàng và nhận tài khoản khi hoàn tất qua API.
- Bảo vệ dòng tiền: Tất cả giao dịch tài chính sử dụng đơn vị số nguyên VND (
integer) không phát sinh lỗi số thực (float). - Giao dịch bất biến (Idempotency): Gửi lại request khi gặp sự cố mạng (timeout) mà không sợ bị trừ tiền lần hai.
Mọi endpoint yêu cầu header Content-Type: application/json khi gửi body và phản hồi luôn ở định dạng JSON với mã trạng thái HTTP tiêu chuẩn.
Chu kỳ đối tác & Cấp khóa an toàn
Tài khoản đối tác được quản lý nghiêm ngặt qua 4 trạng thái chu kỳ hoạt động:
- 1. Pending (Chờ duyệt): Đối tác mới đăng ký, chưa được cấp quyền gọi API (mọi lệnh gọi trả về
403 Forbidden). - 2. Active (Hoạt động): Đã được Quản trị viên duyệt, có khóa API hoạt động và hạn mức giao dịch hợp lệ.
- 3. Suspended (Tạm dừng): Tài khoản bị tạm ngưng giao dịch theo lệnh quản trị hoặc phát hiện bất thường.
- 4. Revoked (Đã thu hồi): Khóa API bị hủy vĩnh viễn, không thể tiếp tục gọi hệ thống.
API Key B2B chỉ được gửi duy nhất qua tin nhắn riêng (Private DM / delivery receipt) trực tiếp từ Telegram Bot chính thức sau khi Quản trị viên phê duyệt. Hệ thống tuyệt đối không hiển thị khóa ở nhóm chung, kênh công khai hay giao diện web.
Quy chuẩn Header xác thực API
| Header | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| X-API-Key | String (64 hex) | REQUIRED | Chuỗi 64 ký tự hexa ngẫu nhiên bảo mật (được sinh từ hàm 32-byte mật mã học). Ví dụ: YOUR_API_KEY |
| X-Idempotency-Key | String | TÙY CHỌN (Mua/Nạp: BẮT BUỘC) | Khóa định danh giao dịch duy nhất do đối tác sinh (UUIDv4 hoặc chuỗi ký tự an toàn) nhằm chống trừ tiền trùng lặp. |
Chế độ hoạt động API & Giới hạn tần suất
Hệ thống B2B vận hành theo 3 chế độ được kiểm soát bởi Quản trị viên:
Trang tài liệu GET /apidocs và route tương thích GET /api/docs là tài nguyên công khai (trả về 307 chuyển hướng tới /apidocs trong mọi chế độ), cho phép đối tác tra cứu tài liệu kỹ thuật ngay cả khi API ở chế độ bảo trì (locked).
Bảng phân quyền trạng thái hệ thống
| Chế độ | Quyền đọc dữ liệu | Giao dịch / Mua hàng | Ý nghĩa vận hành |
|---|---|---|---|
locked (Mặc định) |
Bị chặn (503 Service Unavailable) | Bị chặn (503 Service Unavailable) | Hệ thống đóng băng hoàn toàn. Dùng khi khởi động, bảo trì nâng cấp hoặc xử lý sự cố. |
read_only |
Cho phép (200 OK) | Bị chặn (503 Service Unavailable) | Cho phép đối tác kiểm tra số dư và lấy danh mục, nhưng tạm khóa cổng đặt mua hàng. |
full |
Cho phép (200 OK) | Toàn quyền (200 OK) | Mở toàn bộ cổng mua hàng, nạp tiền và tra cứu thông tin. |
Giới hạn tần suất gửi yêu cầu (Rate Limits)
Hệ thống áp dụng giới hạn tần suất cơ bản theo địa chỉ IP để chống brute-force và bảo vệ hạ tầng. Ngoài ra, mỗi đối tác được cấu hình giới hạn max_requests_per_minute và hạn mức số tiền giao dịch trong ngày do Admin thiết lập.
1. Kiểm tra số dư ví đại lý
Truy vấn số dư ví hiện tại của đối tác. Hỗ trợ bí danh tương thích /api/wallet/balance.
Tham số Headers yêu cầu:
| Header | Kiểu | Bắt buộc |
|---|---|---|
| X-API-Key | String | REQUIRED |
curl -X GET "https://api.uytinnhat.com/api/balance" \ -H "X-API-Key: YOUR_API_KEY"
Phản hồi mẫu (Dữ liệu mẫu synthetic minh họa):
{
"success": true,
"user_id": 990001,
"username": "partner_demo",
"balance_vnd": 500000,
"balance": 500000,
"wallet_balance": 500000
}
2. Danh sách sản phẩm khả dụng
Truy xuất danh sách tất cả các sản phẩm đang mở bán trên hệ thống. Hỗ trợ cả 2 bí danh id và product_id.
curl -X GET "https://api.uytinnhat.com/api/products" \ -H "X-API-Key: YOUR_API_KEY"
Phản hồi mẫu (Dữ liệu mẫu synthetic minh họa):
{
"success": true,
"count": 2,
"products": [
{
"id": "demo_product_01",
"product_id": "demo_product_01",
"name": "Demo Subscription Account (Synthetic)",
"price_vnd": 65000,
"price": 65000,
"stock": 42,
"description": "Synthetic demonstration account fixture."
},
{
"id": "demo_product_02",
"product_id": "demo_product_02",
"name": "Demo Pro License (Synthetic)",
"price_vnd": 120000,
"price": 120000,
"stock": 15,
"description": "Synthetic software key license fixture."
}
]
}
3. Chi tiết sản phẩm theo mã
Lấy thông tin chi tiết, mô tả và tồn kho của một sản phẩm cụ thể bằng mã sản phẩm. Đối tượng trả về chứa cả 2 bí danh id và product_id.
| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| product_id | String | REQUIRED | Mã định danh duy nhất của sản phẩm (ví dụ: demo_product_01). |
curl -X GET "https://api.uytinnhat.com/api/products/demo_product_01" \ -H "X-API-Key: YOUR_API_KEY"
Phản hồi mẫu (Dữ liệu mẫu synthetic minh họa):
{
"success": true,
"product": {
"id": "demo_product_01",
"product_id": "demo_product_01",
"name": "Demo Subscription Account (Synthetic)",
"price_vnd": 65000,
"stock": 42,
"description": "Synthetic demonstration account fixture with full warranty."
}
}
4. Đặt mua tài khoản B2B (Có Idempotency)
Thực hiện mua tài khoản bằng số dư ví. Hỗ trợ bí danh tương thích POST /api/orders. Khóa công khai chỉ chấp nhận trường product_id trong body.
Kết quả xử lý đơn hàng: Phản hồi 200 OK khi hoàn tất giao hàng và thông tin tài khoản được cấp trong mảng items. Trong trường hợp cần đối soát hoặc nhà cung cấp xử lý dở, hệ thống trả về 202 Accepted với items rỗng (đơn hàng ở trạng thái xử lý). Khi lỗi (hết hàng, thiếu số dư...), hệ thống trả về mã lỗi thích hợp.
Cơ chế Replay cùng Key: Gửi lại request với cùng X-Idempotency-Key và cùng payload sẽ trả về kết quả giao dịch ban đầu kèm tài khoản đã cấp (nếu có) và trường "idempotent_replay": true, bảo đảm số dư ví không bị trừ thêm lần 2.
Tham số Headers yêu cầu:
| Header | Kiểu | Bắt buộc |
|---|---|---|
| X-API-Key | String | REQUIRED |
| X-Idempotency-Key | String | REQUIRED |
Tham số Body JSON:
| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| product_id | String | REQUIRED | Mã sản phẩm cần mua (chỉ chấp nhận tên trường product_id). |
| quantity | Integer | OPTIONAL | Số lượng cần mua. Phải là số nguyên JSON thực thụ (integer) từ 1 đến 100. Nếu bỏ qua sẽ nhận giá trị mặc định là 1. |
curl -X POST "https://api.uytinnhat.com/api/buy" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: unique-order-key-sample-001" \
-H "Content-Type: application/json" \
-d '{"product_id": "demo_product_01", "quantity": 1}'
Phản hồi mẫu (Dữ liệu mẫu synthetic minh họa):
{
"success": true,
"order_code": 1717382912345,
"order_id": 1717382912345,
"product_id": "demo_product_01",
"product_name": "Demo Subscription Account (Synthetic)",
"quantity": 1,
"total": 65000,
"status": "completed",
"items": [
"user_demo@example.invalid|DEMO_CREDENTIAL_NOT_REAL"
],
"new_balance": 435000
}
5. Tra cứu đơn hàng & Lấy lại tài khoản
Tra cứu lại thông tin đơn hàng đã mua và lấy lại danh sách tài khoản được cấp. Hỗ trợ lấy toàn bộ danh sách đơn qua GET /api/orders.
Tham số {order_ref} nhận mã đơn hàng (order_code) số nguyên do hệ thống sinh ra (hoặc payment reference/order_id được trả về từ lệnh mua hàng), không sử dụng Idempotency Key làm tham số URL tại đây.
| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| order_ref | Integer / String | REQUIRED | Mã đơn hàng số nguyên order_code (ví dụ: 1717382912345). |
curl -X GET "https://api.uytinnhat.com/api/orders/1717382912345" \ -H "X-API-Key: YOUR_API_KEY"
Phản hồi mẫu (Dữ liệu mẫu synthetic minh họa):
{
"success": true,
"order": {
"order_code": 1717382912345,
"product_id": "demo_product_01",
"product_name": "Demo Subscription Account (Synthetic)",
"quantity": 1,
"total": 65000,
"status": "completed",
"items": [
"user_demo@example.invalid|DEMO_CREDENTIAL_NOT_REAL"
],
"created_at": "2026-09-11 12:00:00"
}
}
6. Tạo link nạp tiền ví tự động (VietQR)
Khởi tạo giao dịch nạp tiền vào ví đại lý. Hệ thống sinh mã thanh toán VietQR và nội dung chuyển khoản tự động. Hỗ trợ bí danh tương thích POST /api/wallet/topup.
Tham số Headers yêu cầu:
| Header | Kiểu | Bắt buộc |
|---|---|---|
| X-API-Key | String | REQUIRED |
| X-Idempotency-Key | String | REQUIRED |
Tham số Body JSON:
| Tham số | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
| amount | Integer | REQUIRED | Số tiền nạp vào ví (VND). BẮT BUỘC là số nguyên JSON thực thụ (integer), tối thiểu 10,000 và tối đa 50,000,000. |
curl -X POST "https://api.uytinnhat.com/api/deposit" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: unique-deposit-key-sample-001" \
-H "Content-Type: application/json" \
-d '{"amount": 500000}'
Phản hồi mẫu (Dữ liệu mẫu synthetic minh họa):
{
"success": true,
"order_code": 1717382999999,
"amount": 500000,
"payment_code": "DH1717382999999",
"payment_url": "https://example.invalid/payment/demo_gateway_session",
"qr_url": "https://example.invalid/qr/synthetic_qr_demo.png",
"bank_info": {
"bank_name": "DEMO_BANK",
"account_number": "0000000000",
"account_name": "SYNTHETIC_ACCOUNT_NAME"
}
}
Mã lỗi & HTTP Status Codes
Các phản hồi lỗi từ hệ thống sử dụng trường error để mô tả nguyên nhân; một số phản hồi bổ sung trường code khi lỗi thuộc các danh mục có cấu trúc định sẵn (như API_LOCKED, API_READ_ONLY, PARTNER_INACTIVE, PARTNER_LIMIT_EXCEEDED, IDEMPOTENCY_KEY_REQUIRED). Khi xảy ra lỗi hệ thống 500, phản hồi sẽ trả về correlation_id để hỗ trợ tra soát kỹ thuật an toàn.
| Mã HTTP | Cấu trúc phản hồi | Nguyên nhân & Hành động xử lý |
|---|---|---|
400 Bad Request |
"error": "..."(hoặc "code": "IDEMPOTENCY_KEY_REQUIRED" / "PARTNER_LIMIT_EXCEEDED") |
Thiếu header X-Idempotency-Key khi mua/nạp; dữ liệu request sai kiểu (số lượng/tiền nạp không phải số nguyên JSON hợp lệ hoặc ngoài phạm vi cho phép); hoặc vượt hạn mức đối tác. |
401 Unauthorized |
"error": "..." |
Thiếu header X-API-Key hoặc API Key không chính xác. Sai 5 lần liên tiếp trong 60 giây sẽ bị tạm khóa IP. |
403 Forbidden |
"error": "...", "code": "PARTNER_INACTIVE" |
Tài khoản đối tác đang ở trạng thái pending (chưa duyệt), suspended (tạm dừng) hoặc revoked (đã thu hồi). |
404 Not Found |
"error": "..." |
Sản phẩm không tồn tại hoặc đã ẩn; đơn hàng không tìm thấy hoặc không thuộc sở hữu của đối tác. |
409 Conflict |
"error": "..." |
Khóa X-Idempotency-Key bị gửi lại kèm nội dung đơn khác với lần gửi đầu tiên, hoặc request cùng khóa đang được xử lý dở. |
429 Too Many Requests |
"error": "..." |
Vượt quá hạn mức tần suất gửi request cho phép. Cần điều chỉnh thuật toán gửi chậm lại. |
500 Internal Error |
"error": "...", "code": "...", "correlation_id": "..." |
Lỗi nội bộ hệ thống. Trả về mã lỗi an toàn và mã đối soát correlation_id. |
503 Service Unavailable |
"error": "...", "code": "API_LOCKED"hoặc "API_READ_ONLY" |
Hệ thống đang ở chế độ bảo trì hoặc khóa theo lệnh Quản trị viên. |
Hướng dẫn phục hồi sự cố mạng (Recovery Guidance)
Nếu bạn gửi đơn mua hàng nhưng gặp lỗi Timeout hoặc mất kết nối HTTP giữa chừng, tuyệt đối không được tự động sinh một Idempotency-Key mới để mua lại. Việc tạo key mới sẽ khiến hệ thống hiểu là một đơn mua mới và trừ thêm tiền vào ví của bạn.
Quy trình xử lý chuẩn khi rớt mạng / HTTP Timeout:
- Bước 1: Lưu trữ mã định danh giao dịch nội bộ của bạn và chuỗi
X-Idempotency-Keyđã gửi. - Bước 2: Gửi lại request
POST /api/buyvới chính xác cùng mộtX-Idempotency-Keyvà cùng body JSON. Hệ thống sẽ trả về ngay kết quả đơn hàng đã tạo (kèm"idempotent_replay": true) mà không trừ thêm tiền vào ví. - Bước 3: Sau khi nhận được phản hồi chứa
order_code(hoặc tra cứu từ danh sáchGET /api/orders), bạn có thể gọiGET /api/orders/{order_code}để tra cứu lại thông tin đơn hàng bất kỳ lúc nào (lưu ý: endpoint này nhận mã đơn hoặc payment reference, không nhận idempotency key).
Quy chuẩn an toàn bảo mật đối tác
- Bảo mật khóa trên máy chủ (Backend-only): API Key chỉ được lưu trữ và gọi từ backend máy chủ của đối tác. Tuyệt đối không nhúng key vào mã nguồn Javascript phía client (Frontend/Mobile app) hoặc kho lưu trữ Git công khai.
- Định dạng tiền tệ chuẩn: Toàn bộ giá và số dư đều tính bằng đồng Việt Nam dạng số nguyên (
integer). Không truyền chuỗi float có dấu chấm thập phân. - Thu hồi & Đổi khóa: Nếu nghi ngờ lộ key, hãy truy cập Telegram Bot ngay lập tức để thực hiện yêu cầu đổi key mới.
Mã nguồn mẫu kết nối B2B
Ví dụ minh họa cách thực hiện kiểm tra số dư và đặt hàng có tích hợp Idempotency:
curl -X GET "https://api.uytinnhat.com/api/balance" \
-H "X-API-Key: YOUR_API_KEY"
# Idempotent Order Placement
ORDER_KEY="partner-order-$(date +%s)"
curl -X POST "https://api.uytinnhat.com/api/buy" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: $ORDER_KEY" \
-H "Content-Type: application/json" \
-d '{"product_id": "demo_product_01", "quantity": 1}'
import uuid
import httpx
API_BASE = "https://api.uytinnhat.com"
API_KEY = "YOUR_API_KEY"
headers = {"X-API-Key": API_KEY}
with httpx.Client(base_url=API_BASE, headers=headers, timeout=30.0) as client:
bal_resp = client.get("/api/balance")
print("Balance:", bal_resp.json().get("balance_vnd"))
idempotency_key = f"order-{uuid.uuid4()}"
buy_resp = client.post(
"/api/buy",
headers={"X-Idempotency-Key": idempotency_key},
json={"product_id": "demo_product_01", "quantity": 1},
)
data = buy_resp.json()
if buy_resp.status_code == 200:
print("Success! Items delivered:", data.get("items"))
print("Order code:", data.get("order_code"))
else:
print("Purchase failed:", data.get("error"))
import crypto from "crypto";
const API_BASE = "https://api.uytinnhat.com";
const API_KEY = "YOUR_API_KEY";
async function main() {
const balRes = await fetch(`${API_BASE}/api/balance`, {
headers: { "X-API-Key": API_KEY }
});
const balData = await balRes.json();
console.log("Balance:", balData.balance_vnd);
const idempKey = `order-${crypto.randomUUID()}`;
const buyRes = await fetch(`${API_BASE}/api/buy`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"X-Idempotency-Key": idempKey,
"Content-Type": "application/json"
},
body: JSON.stringify({ product_id: "demo_product_01", quantity: 1 })
});
const buyData = await buyRes.json();
if (buyRes.ok) {
console.log("Success! Items:", buyData.items);
console.log("Order code:", buyData.order_code);
} else {
console.error("Failed:", buyData.error);
}
}
main();