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

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
lib/hatang.ts
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ọnBiến môi trườngÝ nghĩa
tham số đầu tiênHATANG_API_KEYAPI key (ht_…) có payments:read và payments:write
baseUrlHATANG_BASE_URLhttps://api.hatang.dev
environmentHATANG_ENVIRONMENTdevelopment 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: {
buyer: { name: "Nguyễn Văn Khách", tax_code: "0109876543", email: "[email protected]" },
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.

app/api/hatang/ipn/route.ts (Next.js)
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
}
}
Express
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);
});
cẩn thận

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 ordermetadata.order_id khác order_id bạn truyền
is a <environment> paymentPayment 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àmChứ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 / confirmUnlinkTà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 / historyHó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​

Xem trang này dạng Markdown