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

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:

  1. 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).
  2. 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ườngMô tả
merchant_idMã merchant của store.
api_keyAPI Key (để bạn đối chiếu nếu cần).
request_idMã đơn hàng của bạn (để bạn so khớp với hóa đơn nội bộ).
trans_idMã hóa đơn. Dùng để cập nhật đơn hàng tương ứng.
statusLuôn là "completed".
amountSố tiền USD (string).
token_symbolCoin/Token (ví dụ: USDT).
token_priceTỷ giá tại thời điểm giao dịch (string). Có thể fallback về amount nếu không có.
token_amountSố lượng crypto thực tế được chuyển (string). Có thể là null trong trường hợp hiếm.
network_symbolMạ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_hashHash giao dịch trên blockchain.

Yêu cầu đối với Merchant

  1. Nhận POST tại endpoint tương ứng với callback_url.
  2. Parse JSON body.
  3. Xác minh (tùy chọn nhưng khuyến nghị): kiểm tra merchant_id hoặc trans_id so với dữ liệu nội bộ của bạn.
  4. Cập nhật đơn hàng: đánh dấu đơn hàng khớp với trans_id hoặc request_id là đã thanh toán; lưu transaction_hash nếu cần.
  5. 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ườngMô tả
merchant_idMã merchant của store.
request_idMã đơn hàng của bạn (nếu đã cung cấp khi tạo hóa đơn).
trans_idMã hóa đơn. Dùng để cập nhật đơn hàng tương ứng.
statusLuôn là "expired".
amountSố 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 completed bao gồm api_key trong payload. So sánh với API key bạn đã lưu để xác minh tính xác thực. Lưu ý: webhook expired không bao gồm api_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 1Ngay lập tức (khi hóa đơn hoàn thành)
Lần 2Sau 10 giây
Lần 3Sau 30 giây
Lần 4Sau 30 giây
Lần 5Sau 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.