Môi trường và sandbox
Thanh toán là sản phẩm duy nhất của Hạ Tầng có môi trường. Mỗi doanh nghiệp có môi trường development và production, và dữ liệu thanh toán của hai bên tách biệt hoàn toàn: tài khoản ngân hàng, payment, giao dịch, nhà cung cấp hóa đơn, hóa đơn và khoá tích hợp. API key và webhook endpoint dùng chung. Trang này giải thích cách chọn môi trường và cách test trọn luồng mà không cần tiền thật.
Khác nhau ở đâu
development | production | |
|---|---|---|
| Ngân hàng | Ngân hàng giả lập (simulator): mã xác nhận 000000, chuyển khoản giả lập bằng API | Tài khoản ngân hàng thật của bạn |
| Hóa đơn điện tử | Sandbox của nhà cung cấp (Mắt Bão demo); không gửi cơ quan thuế | Hóa đơn thật theo mã số thuế của bạn |
| Giả lập chuyển khoản | POST /payments/simulate/transfer, cùng nút trên trang checkout và trang payment trên dashboard | Không có (403 environment_mismatch) |
| Khoá tích hợp | ak_test_… / sk_test_… | ak_… / sk_… |
| Chọn bằng | Hatang-Environment: development | Không gửi header (mặc định), hoặc Hatang-Environment: production |
Chọn môi trường cho từng request
Gửi header Hatang-Environment trong mỗi request:
| Header | Kết quả |
|---|---|
Hatang-Environment: development | Request đọc và ghi dữ liệu development |
Hatang-Environment: production hoặc không gửi | Production |
| Giá trị khác | 422 validation_error: "Hatang-Environment must be development or production" |
Mọi phản hồi lặp lại môi trường đã dùng trong header Hatang-Environment, và mọi object thanh toán (payment, giao dịch, kết nối ngân hàng, hóa đơn) có trường environment.
- cURL
- Node.js
- Python
- PHP
curl https://api.hatang.dev/payments \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development"
import { Hatang } from "@hatang/sdk";
// Tùy chọn này được gửi dưới dạng header Hatang-Environment trong mọi request.
// Không có tùy chọn, SDK đọc biến môi trường HATANG_ENVIRONMENT.
const hatang = new Hatang(process.env.HATANG_API_KEY, {
baseUrl: "https://api.hatang.dev",
environment: "development",
});
const { data } = await hatang.payments.list();
import os
from hatang import Hatang
# environment= gửi header Hatang-Environment trong mọi lần gọi
dev = Hatang(os.environ["HATANG_API_KEY"], environment="development")
payments = dev.payments.list() # chỉ thanh toán của môi trường development
<?php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.hatang.dev']);
$res = $client->get('/payments', ['headers' => [
'Authorization' => 'Bearer ' . getenv('HATANG_API_KEY'),
'Hatang-Environment' => 'development',
]]);
echo $res->getHeaderLine('Hatang-Environment'); // development
Đọc môi trường từ cấu hình (HATANG_ENVIRONMENT=development trên server staging, không đặt gì ở production) thay vì viết cứng trong code, để cùng một code chạy được ở cả hai nơi.
Key bắt đầu bằng ht_test_ gắn với development: không gửi header thì dùng development, còn yêu cầu production sẽ trả về 403 environment_mismatch.
Id của môi trường này không tồn tại ở môi trường kia: đọc một payment development khi đang chọn production sẽ trả về 404 not_found.
Trên dashboard
Thanh trên cùng của mọi trang Thanh toán hiển thị môi trường hiện tại và cho phép chuyển. Production có màu đỏ, và chuyển sang production sẽ hỏi xác nhận. Các sản phẩm khác không có nút chuyển này.
Ngân hàng giả lập
Liên kết bằng { "provider": "simulator" } và xác nhận bằng mã 000000 (xem Bắt đầu nhanh). Mặc định, mã QR ghi ngân hàng MB và số tài khoản là số tự sinh (bắt đầu bằng SIM), nên không ngân hàng thật nào nhận ra.
Để có VietQR thật cho ngân hàng của bạn, truyền bank_code và số tài khoản:
{
"provider": "simulator",
"bank_code": "BIDV",
"account_number": "96247001234",
"account_name": "NGUYEN VAN A"
}
bank_code nhận các mã ngắn quen thuộc: MB (mặc định), BIDV, ACB, VCB, ICB, TCB, VPB, TPB, STB và các ngân hàng VietQR khác. Với số tài khoản thật, app ngân hàng của người mua sẽ chuyển tiền thật cho bạn. Nhưng development không tích hợp ngân hàng nào, nên Hạ Tầng không thấy tiền về: payment chỉ chuyển trạng thái khi bạn giả lập chuyển khoản hoặc đánh dấu đã thanh toán.
Giả lập chuyển khoản
- cURL
- Node.js
- Python
- PHP
# Trả đủ một payment
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" }'
# Trả thiếu
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", "amount": 50000 }'
await hatang.payments.simulateTransfer({ payment_id: "pay_123" }); // trả đủ
await hatang.payments.simulateTransfer({ payment_id: "pay_123", amount: 50_000 }); // trả thiếu
// Giao dịch không gắn đơn, cho luồng chỉ nhận giao dịch:
await hatang.payments.simulateTransfer({ connection_id: "bcn_123", amount: 99_000, content: "DH20250919 chuyen tien" });
HEADERS = {"Authorization": f"Bearer {os.environ['HATANG_API_KEY']}", "Hatang-Environment": "development"}
requests.post("https://api.hatang.dev/payments/simulate/transfer", headers=HEADERS,
json={"payment_id": "pay_123", "amount": 50000}, timeout=10).raise_for_status()
$client->post('/payments/simulate/transfer', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('HATANG_API_KEY'),
'Hatang-Environment' => 'development',
],
'json' => ['payment_id' => 'pay_123', 'amount' => 50000],
]);
| Trường | Ý nghĩa |
|---|---|
payment_id | Payment cần trả. Tự điền tài khoản ngân hàng, số tiền còn thiếu và mã |
connection_id | Tài khoản ngân hàng, cho giao dịch không gắn payment. Cần payment_id hoặc connection_id |
amount | Số tiền khác, để test trả thiếu và trả thừa. Mặc định là số tiền còn thiếu |
content | Nội dung chuyển khoản như ngân hàng báo. Mặc định CHUYEN TIEN <code> |
direction | in (mặc định) hoặc out |
reference | Mã tham chiếu ngân hàng. Dùng lại một mã để giả lập ngân hàng đẩy cùng giao dịch hai lần |
counterparty_name | Tên người chuyển. Mặc định NGUYEN VAN KHACH |
Lời gọi trả về 202:
{
"object": "simulated_transfer",
"reference": "SIM1758274452930A1B2C3",
"amount": 50000,
"direction": "in",
"description": "CHUYEN TIEN HTBF7RDGVQ",
"connection_id": "bcn_01a0b90428fd7c499145a12c133abe41",
"duplicate": false
}

Giao dịch giả lập đi qua đúng quy trình như giao dịch từ ngân hàng thật: khớp mã, sự kiện, IPN và webhook. Handler của bạn nhận đúng những gì sẽ nhận ở production.
Lỗi
| Lỗi | Khi nào |
|---|---|
403 environment_mismatch | Request ở production |
422 validation_error | Không có payment_id lẫn connection_id, hoặc tài khoản không phải tài khoản ngân hàng giả lập |
409 invalid_state | Tài khoản ngân hàng không ở trạng thái active |
422 missing_required_field | Không có amount và payment không còn thiếu tiền |
Kịch bản test
| Kịch bản | Cách làm |
|---|---|
| Trả đủ | Giả lập chỉ với payment_id |
| Trả thiếu rồi trả nốt | Giả lập amount nhỏ hơn tổng, rồi giả lập lần nữa không có amount |
| Trả thừa | Giả lập amount lớn hơn tổng |
| Hết hạn | Tạo với expires_in: 60, đợi vài phút, kiểm tra payment.expired |
| Tiền về sau khi hết hạn | Giả lập trên payment đã hết hạn: payment chuyển sang paid |
| Người mua quên ghi mã | Giả lập với connection_id và content không chứa mã |
| Ngân hàng đẩy trùng | Giả lập hai lần cùng reference và amount: lần hai trả về duplicate: true và không thay đổi gì |
| Endpoint của bạn bị lỗi | Đặt ipn_url trỏ tới URL trả 500, rồi xem GET /payments/{id}/ipn-deliveries |
Handler của bạn phải kiểm tra environment. Một payment development không bao giờ được mở khóa đơn hàng thật.
Bước tiếp theo
- Liên kết tài khoản ngân hàng ở production.
- Tạo thanh toán.
- Lên production: chuyển sang production từng bước.