SDK JavaScript
@hatang/sdk gói gọn toàn bộ tích hợp Thanh toán: tạo đơn, xác nhận thanh toán, xác minh IPN và webhook. Mọi hàm API trả về { data, error } và không throw khi API báo lỗi. Các hàm xác minh chỉ dùng Web Crypto nên chạy được cả trên Bun, Deno, Vercel và Cloudflare Workers.
Trang này nói về phần thanh toán. Bản thân client (tùy chọn, lỗi, các sản phẩm khác) được mô tả ở SDK Node.js.
Cài đặt và cấu hình
npm install @hatang/sdk
import { Hatang } from "@hatang/sdk";
export const hatang = new Hatang(process.env.HATANG_API_KEY, {
baseUrl: "https://api.hatang.dev",
// Bỏ ở production, hoặc đặt HATANG_ENVIRONMENT=development thay cho dòng này.
environment: "development",
});
| Tùy chọn | Biến môi trường | Ý nghĩa |
|---|---|---|
| tham số đầu tiên | HATANG_API_KEY | API key (ht_…) có payments:read và payments:write |
baseUrl | HATANG_BASE_URL | https://api.hatang.dev |
environment | HATANG_ENVIRONMENT | development hoặc production, gửi dưới dạng header Hatang-Environment |
1. Tạo payment cho đơn hàng
createOrder dùng mã đơn làm idempotency key: gọi hai lần cho cùng một đơn (bấm đúp, thử lại request) trả về cùng một payment. Mã đơn cũng được lưu vào metadata.order_id.
const { data: payment, error } = await hatang.payments.createOrder({
order_id: "SO-1001",
amount: 150_000,
description: "Đơn hàng #1001",
return_url: "https://shop.example.vn/orders/SO-1001",
ipn_url: "https://shop.example.vn/api/hatang/ipn",
// Tùy chọn: phát hành hóa đơn điện tử khi đã thanh toán.
invoice: {
items: [{ name: "Gói dịch vụ", quantity: 1, unit_price: 136_364, vat_rate: 10 }],
},
});
if (error) throw new Error(`${error.name}: ${error.message}`);
// Chuyển người mua tới trang checkout…
redirect(payment.checkout_url);
// …hoặc tự hiển thị QR: <img src={payment.qr.image_url} />, ngân hàng ở payment.bank, mã ở payment.code.
Thêm code: "SO1001" để dùng mã đơn của bạn làm nội dung chuyển khoản thay cho mã HT… tự sinh. Xem Mã thanh toán.
Gọi lại createOrder cho cùng order_id nhưng với trường khác sẽ trả về invalid_idempotent_request. Khi đơn thay đổi, hãy dùng mã đơn mới.
2. Nhận IPN
Truyền body gốc và headers. constructIpnEvent kiểm tra chữ ký HMAC và trả về lời gọi đã parse, hoặc throw HatangSignatureError.
import { constructIpnEvent, HatangSignatureError } from "@hatang/sdk";
export async function POST(req: Request) {
const raw = await req.text(); // trước khi parse JSON
try {
const ipn = await constructIpnEvent(raw, req.headers, process.env.HATANG_SECRET_KEY!);
if (await alreadyHandled(ipn.id)) return new Response(null, { status: 200 });
const order = ipn.data.metadata.order_id as string;
if (ipn.data.status === "paid" || ipn.data.status === "overpaid") await markOrderPaid(order, ipn.data.paid_amount);
if (ipn.data.invoice?.status === "issued") await saveInvoiceNumber(order, ipn.data.invoice.number);
return new Response(null, { status: 200 });
} catch (err) {
if (err instanceof HatangSignatureError) return new Response(null, { status: 401 });
throw err; // 5xx khiến Hatang thử lại sau
}
}
import express from "express";
import { constructIpnEvent } from "@hatang/sdk";
app.post("/api/hatang/ipn", express.raw({ type: "application/json" }), async (req, res) => {
const ipn = await constructIpnEvent(req.body, req.headers, process.env.HATANG_SECRET_KEY!).catch(() => null);
if (!ipn) return res.sendStatus(401);
await queue.add("hatang-ipn", ipn); // xử lý nền, trả lời nhanh
res.sendStatus(200);
});
express.json() sẽ parse rồi serialize lại body, khiến chữ ký không còn khớp. Hãy dùng express.raw() cho route IPN.
3. Xác nhận trước khi giao hàng
Khi người mua quay lại return_url, hỏi Hạ Tầng thay vì tin trình duyệt. verify chỉ trả verified: true khi payment đã paid (hoặc overpaid) và khớp với thông tin bạn truyền:
const { data: check, error } = await hatang.payments.verify(paymentId, {
order_id: "SO-1001",
amount: 150_000,
environment: "production", // payment development không bao giờ mở khóa đơn thật
});
if (error) return showError(error.message);
if (check.verified) await fulfil("SO-1001");
else console.log(check.reasons); // vd ["status is partially_paid"], ["belongs to another order"]
Giá trị trong reasons | Ý nghĩa |
|---|---|
status is <status> | Chưa paid hoặc overpaid |
paid <n>, expected <amount> | Đã trả, nhưng ít hơn số tiền bạn cần |
belongs to another order | metadata.order_id khác order_id bạn truyền |
is a <environment> payment | Payment thuộc môi trường kia |
4. Webhook (tùy chọn)
Webhook nhận mọi sự kiện của doanh nghiệp, không chỉ một payment:
import { constructWebhookEvent, type Payment } from "@hatang/sdk";
const event = await constructWebhookEvent<Payment>(raw, req.headers, process.env.HATANG_WEBHOOK_SECRET!); // whsec_…
if (event.type === "payment.succeeded") await markOrderPaid(event.data.metadata.order_id as string, event.data.paid_amount);
constructWebhookEvent throw HatangSignatureError khi chữ ký sai hoặc timestamp lệch quá 5 phút. Truyền { toleranceSec } làm tham số thứ tư để đổi khoảng này.
5. Test ở development
Với environment: "development" và đã liên kết ngân hàng giả lập:
await hatang.payments.simulateTransfer({ payment_id: payment.id }); // trả đủ
await hatang.payments.simulateTransfer({ payment_id: payment.id, amount: 50_000 }); // trả thiếu
Xử lý lỗi
const { data, error } = await hatang.payments.create({ amount: 150_000, code: "SO1001" });
if (error) {
switch (error.name) {
case "code_taken": // 409: mã đã được dùng trên tài khoản ngân hàng này
case "no_bank_connection": // 422: cần liên kết tài khoản ngân hàng trước
case "validation_error": // 422: error.message nêu trường bị sai
return showError(error.message);
case "rate_limit_exceeded": // 429: giảm tốc độ rồi thử lại
return retryLater();
default:
throw new Error(`${error.statusCode} ${error.name}: ${error.message}`);
}
}
Lỗi mạng trả về error.name === "application_error" với statusCode: 0. Danh sách đầy đủ ở Lỗi.
Danh sách hàm
| Hàm | Chức năng |
|---|---|
payments.createOrder({ order_id, amount, … }) | Tạo payment cho đơn; an toàn khi gọi lại |
payments.create(payload, { idempotencyKey }) | Như trên, với key và metadata của bạn |
payments.verify(id, { order_id, amount, environment }) | { verified, paid, reasons, payment } |
payments.get(id) / list(params) | Đọc payment |
payments.cancel(id) / markPaid(id, { amount, note }) | Hủy payment đang chờ, hoặc ghi nhận tiền nhận ngoài ngân hàng |
payments.ipnDeliveries(id) | Các lần gửi IPN của một payment |
payments.qr({ amount, content, connection_id }) | VietQR cho luồng chỉ nhận giao dịch |
payments.simulateTransfer(…) | Chỉ ở development |
payments.transactions.list(params) / get(id) | Giao dịch ngân hàng |
payments.bankConnections.list / get / create / confirm / update / unlink / confirmUnlink | Tài khoản ngân hàng |
payments.statements.import({ connection_id, content }) / get(id) | Nhập sao kê |
einvoices.create / get / list / update / remove / publish / retry / replace / resolve / sync / history | Hóa đơn điện tử |
einvoices.providers() / einvoices.connections.* | Tài khoản nhà cung cấp hóa đơn |
constructIpnEvent(raw, headers, secretKey) / verifyIpn(raw, signature, secretKey) | Chữ ký IPN (HMAC-SHA256) |
constructWebhookEvent(raw, headers, secret) / verifyWebhook(raw, headers, secret) | Chữ ký webhook (Svix), cửa sổ 5 phút |
Bước tiếp theo
- Xác minh IPN không cần SDK.
- Tạo thanh toán: mọi trường và trạng thái.
- SDK Node.js: client cho mọi sản phẩm Hạ Tầng.