Webhook
Webhook cho phép hệ thống của bạn nhận thông báo tự động theo thời gian thực từ PayerScan. Hệ thống gửi các yêu cầu POST đến callback_url mà Merchant cung cấp khi tạo hóa đơn.
Loại sự kiện
PayerScan có 2 sự kiện chính được trả về trong trường status:
completed— Thanh toán thành công: Một giao dịch khớp đã được xác nhận với hóa đơn (blockchain hoặc Binance Pay).expired— Hóa đơn hết hạn: Hóa đơn đã vượt quá thời gian chờ thanh toán.
Merchant cần cung cấp một URL công khai (khuyến nghị HTTPS), nhận các yêu cầu POST, xử lý payload và trả về HTTP 2xx trong thời gian timeout quy định (ví dụ: 10 giây).
Cấu trúc Payload
1. Webhook khi thanh toán thành công (status: completed)
Khi tìm thấy giao dịch khớp (đúng địa chỉ, đúng số tiền, trong khung thời gian), hệ thống cập nhật hóa đơn thành completed và gửi POST đến callback_url với payload sau:
Payload (JSON body)
{
"merchant_id": "MERCHANT_001",
"api_key": "YOUR_API_KEY",
"request_id": "order-1234",
"trans_id": "TID-ABC123DEF4567890",
"status": "completed",
"amount": "100",
"token_symbol": "USDT",
"token_price": "1",
"token_amount": "100.0026",
"network_symbol": "BSC",
"from_address": "0x8894e0a0c962cb723c1976a4421c95949be2d4e3",
"to_address": "0xc221460115e2CfCa5bF089A7e647b11cb9631efE",
"transaction_hash": "0xd346b32b83b35376d42a2464598fbf565fffb39e6569200f034a5e8342c532d7"
}
| Trường | Mô tả |
|---|---|
merchant_id | Mã merchant của store. |
api_key | API Key (để bạn đối chiếu nếu cần). |
request_id | Mã đơn hàng của bạn (để bạn so khớp với hóa đơn nội bộ). |
trans_id | Mã hóa đơn. Dùng để cập nhật đơn hàng tương ứng. |
status | Luôn là "completed". |
amount | Số tiền USD (string). |
token_symbol | Coin/Token (ví dụ: USDT). |
token_price | Tỷ giá tại thời điểm giao dịch (string). Có thể fallback về amount nếu không có. |
token_amount | Số lượng crypto thực tế được chuyển (string). Có thể là null trong trường hợp hiếm. |
network_symbol | Mạng lưới (ví dụ: BSC, TRC20, ETH, SOL, BINANCE_ID, OKX_UID, BYBIT_UID, ...). |
from_address | Địa chỉ ví người gửi. |
to_address | Địa chỉ ví nhận (của Store). |
transaction_hash | Hash giao dịch trên blockchain. |
Yêu cầu đối với Merchant
- Nhận POST tại endpoint tương ứng với
callback_url. - Parse JSON body.
- Xác minh (tùy chọn nhưng khuyến nghị): kiểm tra
merchant_idhoặctrans_idso với dữ liệu nội bộ của bạn. - Cập nhật đơn hàng: đánh dấu đơn hàng khớp với
trans_idhoặcrequest_idlà đã thanh toán; lưutransaction_hashnếu cần. - Trả về HTTP 2xx (ví dụ: 200 OK) trong thời gian timeout (ví dụ: 10 giây). Không cần response body; hệ thống chỉ cần status 2xx để coi webhook là thành công.
Nếu bạn trả về 4xx/5xx hoặc timeout, hệ thống đánh dấu callback là failed và có cơ chế thử lại (số lần thử và khoảng cách phụ thuộc vào cấu hình backend).
2. Webhook khi hóa đơn hết hạn (status: expired)
Khi hóa đơn chuyển sang trạng thái expired (quá expires_at), hệ thống gửi POST đến callback_url với payload đơn giản hơn:
Payload (JSON body)
{
"merchant_id": "MERCHANT_001",
"request_id": "order-1234",
"trans_id": "TID-ABC123DEF4567890",
"status": "expired",
"amount": "100"
}
| Trường | Mô tả |
|---|---|
merchant_id | Mã merchant của store. |
request_id | Mã đơn hàng của bạn (nếu đã cung cấp khi tạo hóa đơn). |
trans_id | Mã hóa đơn. Dùng để cập nhật đơn hàng tương ứng. |
status | Luôn là "expired". |
amount | Số tiền USD (string). |
Bạn có thể dùng thông tin này để cập nhật đơn hàng thành "hết hạn", hủy đặt chỗ, v.v. Bạn vẫn nên trả về HTTP 2xx để hệ thống không coi đó là lỗi.
3. Ví dụ xử lý Webhook (Node.js)
app.post('/webhook/payment', express.json(), async (req, res) => {
const { merchant_id, api_key, trans_id, request_id, status, amount, transaction_hash } = req.body;
// Xác minh merchant_id (cả completed và expired)
if (merchant_id !== process.env.PAYERSCAN_MERCHANT_ID) {
return res.status(401).send('Unauthorized');
}
// Xác minh api_key (chỉ completed — expired không có api_key)
if (status === 'completed' && api_key !== process.env.PAYERSCAN_API_KEY) {
return res.status(401).send('Unauthorized');
}
// Tìm đơn hàng theo request_id hoặc trans_id
const order = await orderService.findByRequestId(request_id) || await orderService.findByTransId(trans_id);
if (!order) {
return res.status(404).send('Order not found');
}
if (status === 'completed') {
await orderService.markPaid(order, { transaction_hash, amount });
} else if (status === 'expired') {
await orderService.markExpired(order);
}
res.status(200).send('OK');
});
Xác minh chữ ký
Để đảm bảo webhook thực sự đến từ PayerScan, bạn nên xác minh chữ ký hoặc đối chiếu dữ liệu.
- HTTPS: Luôn sử dụng HTTPS cho
callback_urlđể ngăn chặn nghe lén. - Xác minh API Key (chỉ completed): Webhook
completedbao gồmapi_keytrong payload. So sánh với API key bạn đã lưu để xác minh tính xác thực. Lưu ý: webhookexpiredkhông bao gồmapi_key.
Tính Idempotent (Lũy đẳng)
Một trans_id chỉ nhận một sự kiện completed, nhưng cơ chế retry có thể gửi lại. Hãy đảm bảo logic xử lý của bạn có tính idempotent (cập nhật nhiều lần vẫn an toàn, không ghi nhận trùng lặp).
Thử lại khi Webhook thất bại
Khi server của Merchant trả về lỗi (4xx/5xx) hoặc timeout, hệ thống thử lại webhook theo lịch sau:
| Lần thử | Thời điểm |
|---|---|
| Lần 1 | Ngay lập tức (khi hóa đơn hoàn thành) |
| Lần 2 | Sau 10 giây |
| Lần 3 | Sau 30 giây |
| Lần 4 | Sau 30 giây |
| Lần 5 | Sau 60 giây |
Tối đa 5 lần thử. Sau 5 lần thất bại, callback_status chuyển thành failed và không thử lại nữa.
Merchant nên đảm bảo endpoint ổn định và trả về 2xx nhanh chóng (< 10 giây) để tránh thử lại không cần thiết.