Bắt đầu nhanh
Nhận khoản thanh toán đầu tiên ở môi trường development: liên kết ngân hàng giả lập, tạo payment, giả lập người mua chuyển tiền, rồi nhận lời gọi có chữ ký trên server của bạn. Bạn không cần tài khoản ngân hàng thật.
Trước khi bắt đầu
- Tài khoản Hạ Tầng đã bật module Thanh toán (Cài đặt doanh nghiệp → Module). Khi module tắt, mọi request Thanh toán trả về
403 product_disabled. - Một server nhận được
POSTHTTPS từ internet, dùng ở bước 6. Khi chạy trên máy cá nhân, dùng tunnel như ngrok hoặc Cloudflare Tunnel.
Mọi request trên trang này đều gửi Hatang-Environment: development. Không có header này, request sẽ đi vào production. Xem Môi trường và sandbox.
Bước 1: Tạo API key
Trên dashboard, mở API Keys và tạo key có scope payments:read và payments:write. Chỉ giữ key trên server.
export HATANG_API_KEY=ht_123...
Với Node.js, cài SDK:
npm install @hatang/sdk
import { Hatang } from "@hatang/sdk";
export const hatang = new Hatang(process.env.HATANG_API_KEY, {
baseUrl: "https://api.hatang.dev",
environment: "development", // gửi dưới dạng header Hatang-Environment
});
Bước 2: Liên kết ngân hàng giả lập
Bắt đầu liên kết, rồi xác nhận bằng mã 000000. Ngân hàng giả lập luôn dùng mã này.
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments/bank-connections \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development" \
-H "Content-Type: application/json" \
-d '{ "provider": "simulator", "label": "Tài khoản thử" }'
curl https://api.hatang.dev/payments/bank-connections/bcn_123/confirm \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development" \
-H "Content-Type: application/json" \
-d '{ "otp": "000000" }'
import { hatang } from "./lib/hatang";
const { data: conn, error } = await hatang.payments.bankConnections.create({ provider: "simulator", label: "Tài khoản thử" });
if (error) throw new Error(`${error.name}: ${error.message}`);
await hatang.payments.bankConnections.confirm(conn.id, "000000");
import os, requests
API = "https://api.hatang.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}", "Hatang-Environment": "development"}
conn = requests.post(f"{API}/payments/bank-connections", headers=HEADERS,
json={"provider": "simulator", "label": "Tài khoản thử"}, timeout=10).json()
requests.post(f"{API}/payments/bank-connections/{conn['id']}/confirm", headers=HEADERS,
json={"otp": "000000"}, timeout=10).raise_for_status()
<?php
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.hatang.dev',
'headers' => [
'Authorization' => 'Bearer ' . getenv('HATANG_API_KEY'),
'Hatang-Environment' => 'development',
],
]);
$conn = json_decode($client->post('/payments/bank-connections', [
'json' => ['provider' => 'simulator', 'label' => 'Tài khoản thử'],
])->getBody(), true);
$client->post("/payments/bank-connections/{$conn['id']}/confirm", ['json' => ['otp' => '000000']]);
Lời gọi đầu trả về 201 với kết nối ở trạng thái pending_otp kèm challenge; sau khi xác nhận, kết nối chuyển sang active:
{
"object": "bank_connection",
"id": "bcn_01a0b90428fd7c499145a12c133abe41",
"environment": "development",
"provider": "simulator",
"bank": { "bin": "970422", "name": "MBBank" },
"account_number": "SIM4821937506",
"account_name": "TAI KHOAN GIA LAP",
"account_type": "personal",
"status": "active",
"label": "Tài khoản thử",
"link_mode": "otp",
"activated_at": "2026-09-19T09:30:02.114Z",
"created_at": "2026-09-19T09:29:51.402Z"
}
Bạn cũng làm được việc này trên dashboard: Cài đặt thanh toán → Tài khoản ngân hàng → Thêm tài khoản ngân hàng.
Bước 3: Tạo payment
Gửi Idempotency-Key (mã đơn là lựa chọn tốt) để request thử lại trả về cùng một payment thay vì tạo cái thứ hai.
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development" \
-H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" \
-d '{
"amount": 150000,
"description": "Đơn hàng #1001",
"ipn_url": "https://shop.example.vn/hatang/ipn",
"metadata": { "order_id": "1001" }
}'
// createOrder dùng mã đơn làm Idempotency-Key và lưu vào metadata.order_id.
const { data: payment, error } = await hatang.payments.createOrder({
order_id: "1001",
amount: 150_000,
description: "Đơn hàng #1001",
ipn_url: "https://shop.example.vn/hatang/ipn",
});
if (error) throw new Error(`${error.name}: ${error.message}`);
console.log(payment.checkout_url);
from hatang import Hatang # pip install hatang
hatang = Hatang(environment="development") # gửi dưới dạng header Hatang-Environment
payment = hatang.payments.create({
"amount": 150000,
"description": "Đơn hàng #1001",
"ipn_url": "https://shop.example.vn/hatang/ipn",
"metadata": {"order_id": "1001"},
}, idempotency_key="order-1001")
print(payment["checkout_url"])
$payment = json_decode($client->post('/payments', [
'headers' => ['Idempotency-Key' => 'order-1001'],
'json' => [
'amount' => 150000,
'description' => 'Đơn hàng #1001',
'ipn_url' => 'https://shop.example.vn/hatang/ipn',
'metadata' => ['order_id' => '1001'],
],
])->getBody(), true);
echo $payment['checkout_url'];
Phản hồi là 201 với payment ở trạng thái pending, kèm code, qr và checkout_url. Object đầy đủ được mô tả ở Tạo thanh toán.
Bước 4: Mở trang checkout
Mở checkout_url trên trình duyệt. Trang hiển thị VietQR, thông tin ngân hàng và đồng hồ đếm ngược. Ở development, trang có thêm nút giả lập chuyển khoản.
Bước 5: Giả lập chuyển khoản
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments/simulate/transfer \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development" \
-H "Content-Type: application/json" \
-d '{ "payment_id": "pay_123" }'
await hatang.payments.simulateTransfer({ payment_id: payment.id });
requests.post(f"{API}/payments/simulate/transfer", headers=HEADERS,
json={"payment_id": payment["id"]}, timeout=10).raise_for_status()
$client->post('/payments/simulate/transfer', ['json' => ['payment_id' => $payment['id']]]);
Lời gọi trả về 202. Trong vài giây, payment chuyển sang paid, trang checkout chuyển sang trạng thái đã thanh toán, và ipn_url của bạn nhận lời gọi payment.succeeded.
Bước 6: Xử lý IPN
Endpoint của bạn phải kiểm tra header Hatang-Signature với body gốc trước khi tin bất cứ điều gì, rồi trả 2xx trong 10 giây. Secret key nằm ở Cài đặt thanh toán → Khoá tích hợp (key development bắt đầu bằng sk_test_).
import { constructIpnEvent, HatangSignatureError } from "@hatang/sdk";
export async function POST(req: Request) {
const raw = await req.text(); // body gốc, trước khi parse JSON
try {
const ipn = await constructIpnEvent(raw, req.headers, process.env.HATANG_SECRET_KEY!);
if (ipn.data.status === "paid" || ipn.data.status === "overpaid") {
await markOrderPaid(ipn.data.metadata.order_id as string, ipn.data.paid_amount);
}
return new Response(null, { status: 200 });
} catch (err) {
if (err instanceof HatangSignatureError) return new Response(null, { status: 401 });
throw err; // trả 5xx để Hatang thử lại sau
}
}
Xác minh IPN có handler tương tự bằng PHP, Python và C#.
Bước 7: Xem những gì đã gửi
Mọi lời gọi IPN và kết quả của nó được liệt kê theo payment, mới nhất trước:
curl https://api.hatang.dev/payments/pay_123/ipn-deliveries \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development"
{
"object": "list",
"data": [
{
"object": "ipn_delivery",
"id": "ipn_01a0b90429d87cf89e37fc2f66040ec5",
"payment_id": "pay_01a0b904297873e8b3524a26fdca0ab5",
"event_type": "payment.succeeded",
"url": "https://shop.example.vn/hatang/ipn",
"status": "success",
"attempt": 1,
"response_status": 200,
"response_ms": 84,
"last_error": null,
"next_attempt_at": null,
"delivered_at": "2026-09-19T09:34:13.041Z",
"created_at": "2026-09-19T09:34:12.952Z"
}
]
}
Trang chi tiết payment trên dashboard hiển thị cùng danh sách này.
Lỗi thường gặp
| Lỗi | Nguyên nhân | Cách xử lý |
|---|---|---|
422 no_bank_connection | Chưa có tài khoản ngân hàng nào đang hoạt động ở môi trường này | Làm bước 2, hoặc kiểm tra đã gửi Hatang-Environment: development chưa |
422 validation_error kèm "Several bank accounts are linked; pass connection_id" | Có nhiều hơn một tài khoản đang hoạt động | Gửi connection_id khi tạo payment |
422 validation_error ở ipn_url | URL là địa chỉ nội bộ (localhost, 10.x, 192.168.x…) hoặc không phải https | Dùng URL HTTPS công khai, ví dụ qua tunnel |
401 restricted_api_key | Key thiếu scope Thanh toán | Tạo key có payments:read và payments:write |
403 environment_mismatch | Giả lập chuyển khoản ngoài môi trường development | Gửi Hatang-Environment: development |
| Không nhận được IPN | Endpoint không trả 2xx | Xem last_error ở bước 7; Hạ Tầng thử lại trong khoảng một ngày |
Bước tiếp theo
- Tạo thanh toán: mọi trường, trạng thái và tình huống đặc biệt.
- Nhận kết quả: so sánh IPN, webhook và polling.
- SDK JavaScript: toàn bộ luồng trong vài lời gọi.
- Lên production khi đã sẵn sàng nhận tiền thật.