HTTP POST · JSON
Luồng webhook
Khi tài khoản phát sinh giao dịch, Thuê API Bank gửi payload JSON đến URL bạn đã đăng ký. Endpoint kiểm tra chữ ký, lưu giao dịch và trả HTTP 200 để xác nhận đã nhận.
1Giao dịch mớiNgân hàng ghi nhận tiền vào
→2POST webhookHệ thống gửi JSON + signature
→3HTTP 200Ứng dụng xác nhận đã nhận
Cấu hình endpoint
- 1Tạo URL HTTPS công khai
Ví dụ
https://example.com/webhooks/bank. URL không nên chuyển hướng. - 2Thêm Webhook URL
Đăng nhập hệ thống, chọn Webhooks và gắn URL với tài khoản cần nhận giao dịch.
- 3Lưu signature bí mật
Lưu giá trị được cấp trong biến môi trường phía máy chủ.
- 4Gửi dữ liệu kiểm thử
Dùng chức năng kiểm thử để xác minh endpoint trả mã 200.
Payload giao dịch
{
"status": "success",
"message": "Thành công",
"transactions": [
{
"type": "IN",
"transactionID": "24213",
"amount": "100000",
"description": "DH1024 THANH TOAN"
}
]
}| Trường | Kiểu | Mô tả |
|---|---|---|
type | string | Loại giao dịch IN hoặc OUT. |
transactionID | string | Mã giao dịch dùng để chống xử lý trùng. |
amount | string/number | Số tiền giao dịch. |
description | string | Nội dung chuyển khoản. |
Xác thực header signature
So sánh signature nhận được với secret đã lưu bằng hàm so sánh constant-time. Không ghi secret vào log hoặc source code.
<?php
header('Content-Type: application/json; charset=utf-8');
$secret = $_ENV['THUE_API_BANK_WEBHOOK_SECRET'];
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
if (!hash_equals($secret, $signature)) {
http_response_code(401);
echo json_encode(['status' => false]);
exit;
}
$payload = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
foreach ($payload['transactions'] ?? [] as $transaction) {
// Lưu transactionID với UNIQUE INDEX trước khi xử lý đơn hàng.
}
http_response_code(200);
echo json_encode(['status' => true, 'msg' => 'OK']);import crypto from 'node:crypto';
app.post('/webhooks/bank', express.json(), async (req, res) => {
const actual = Buffer.from(req.get('signature') || '');
const expected = Buffer.from(process.env.WEBHOOK_SECRET || '');
if (actual.length !== expected.length ||
!crypto.timingSafeEqual(actual, expected)) {
return res.sendStatus(401);
}
for (const transaction of req.body.transactions || []) {
await saveOnce(transaction.transactionID, transaction);
}
return res.status(200).json({ status: true, msg: 'OK' });
});Phản hồi và cơ chế gửi lại
Endpoint cần trả mã HTTP 200 sau khi đã lưu giao dịch. Nếu timeout hoặc trả mã khác 200, hệ thống sẽ thử gửi lại sau khoảng một phút, tối đa 10 lần.
Chỉ xác thực và lưu sự kiện trong request webhook. Tác vụ nặng như gửi email, xuất hóa đơn hoặc gọi dịch vụ khác nên đưa vào hàng đợi.
Chống xử lý giao dịch trùng
Webhook có cơ chế retry nên cùng một transactionID có thể được gửi nhiều lần. Hãy đặt unique index trong cơ sở dữ liệu và chỉ hoàn tất đơn ở lần ghi đầu tiên.
CREATE TABLE bank_transactions (
transaction_id VARCHAR(100) PRIMARY KEY,
amount DECIMAL(18, 2) NOT NULL,
description TEXT,
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);