Webhook
Cách nhận thông báo giao dịch từ PayFS qua webhook.
Khi tài khoản ngân hàng có biến động, PayFS gửi một HTTP POST tới endpoint của bạn.
Chưa dựng endpoint bao giờ, đọc Bắt đầu nhanh trước.
Payload
{
"account_id": "1418079746853494784",
"amount": 14000,
"bank": "MB",
"bank_account_number": "0933723830",
"content": "NGUYEN VAN A chuyen tien Ma giao dich Trace773231",
"transaction_date": "2025-09-15T15:02:00.000Z",
"transaction_id": "1418108930751619072",
"transfer_type": "credit"
}| Trường | Kiểu | Mô tả |
|---|---|---|
transaction_id | string | ID giao dịch, duy nhất toàn hệ thống. Dùng làm khóa chống trùng. |
account_id | string | ID tài khoản ngân hàng trong PayFS |
amount | number | Số tiền, đơn vị VND, luôn là số nguyên dương |
transfer_type | string | credit là tiền vào, debit là tiền ra |
content | string | Nội dung chuyển khoản do ngân hàng trả về |
transaction_date | string | Thời điểm giao dịch, ISO 8601 theo giờ UTC |
bank_account_number | string | Số tài khoản nhận hoặc gửi |
bank | string | Mã ngân hàng: MB, ACB, OCB |
amount luôn dương. Dùng transfer_type để biết tiền vào hay ra, đừng dựa vào dấu.
Sự kiện
Khi tạo webhook, bạn chọn sự kiện muốn nhận.
| Sự kiện | Khi nào | Payload |
|---|---|---|
transaction.credit | Tiền vào tài khoản | Như trên |
transaction.debit | Tiền ra khỏi tài khoản | Như trên |
order.success | Đơn hàng thanh toán thành công | Xem dưới |
order.failed | Đơn hàng thanh toán thất bại | Xem dưới |
Hai sự kiện order.* dùng cấu trúc khác:
{
"order_id": "1418108930751619072",
"amount": 250000,
"status": "success",
"customer_email": "khachhang@example.com",
"created_at": "2025-09-15T15:02:00.000Z"
}Chỉ order_id, amount và status là bắt buộc. Ba trường còn lại có thể vắng.
order.success và order.failed là sự kiện webhook của Workspace. Chúng không đổi trong bản này. Callback của một Liên kết thanh toán là HTTPS POST riêng tới Callback URL của liên kết đó, không phải hai sự kiện này. Xem Liên kết thanh toán.
Header
PayFS gửi kèm 5 header sau mỗi lần gọi.
| Header | Ví dụ | Dùng để |
|---|---|---|
X-Client-API-Key | pk_1234... | Xác thực nguồn gọi. Luôn kiểm tra. |
X-PayFS-Signature | 86f02cef... | Chữ ký HMAC-SHA256, hex thô 64 ký tự |
X-PayFS-Timestamp | 1758173916 | UNIX timestamp, dùng chung với chữ ký |
X-PayFS-Webhook-ID | 1418108... | ID của webhook endpoint |
X-PayFS-Attempt | 1 | Lần gửi thứ mấy |
Kiểm tra API key là bắt buộc. Kiểm tra chữ ký là tùy chọn nhưng nên làm, xem Chữ ký webhook.
Xác thực bằng API key
app.post('/payfs-webhook', (req, res) => {
const apiKey = req.headers['x-client-api-key'];
if (!apiKey || apiKey !== process.env.PAYFS_WEBHOOK_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
handleTransaction(req.body);
res.status(200).json({ status: 'ok' });
});<?php
$apiKey = $_SERVER['HTTP_X_CLIENT_API_KEY'] ?? null;
if (!$apiKey || !hash_equals($_ENV['PAYFS_WEBHOOK_API_KEY'], $apiKey)) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$transaction = json_decode(file_get_contents('php://input'), true);
handleTransaction($transaction);
http_response_code(200);
echo json_encode(['status' => 'ok']);Chống xử lý trùng
PayFS gửi lại khi thất bại, nên cùng một giao dịch có thể tới nhiều lần. Dùng
transaction_id làm khóa chống trùng.
if (await daXuLy(tx.transaction_id)) {
return res.status(200).json({ status: 'already_processed' });
}
await danhDauDaXuLy(tx.transaction_id);Lưu vào cơ sở dữ liệu hoặc Redis, đừng lưu trong bộ nhớ tiến trình.
Gửi lại khi thất bại
PayFS coi là thành công khi endpoint trả mã 2xx.
| Thông số | Mặc định |
|---|---|
| Thời gian chờ mỗi lần gửi | 30 giây |
| Số lần thử tối đa | 10 |
| Khoảng chờ lần đầu | 10 giây |
| Hệ số tăng khoảng chờ | 2 |
Endpoint phải trả lời trong 30 giây. Xử lý lâu thì trả 200 trước rồi đẩy việc sang hàng đợi nền. Trả lời chậm bị tính là thất bại và PayFS sẽ gửi lại.
Mã trạng thái nên trả về
| Tình huống | Mã |
|---|---|
| Xử lý xong | 200 |
| Thiếu hoặc sai API key | 401 |
| Payload không hợp lệ | 400 |
| Lỗi tạm thời phía bạn | 500, PayFS sẽ gửi lại |
Xử lý sự cố
Thiếu header X-Client-API-Key
Đa số framework chuẩn hóa tên header về chữ thường. Đọc x-client-api-key.
Sai API key Đối chiếu lại với giá trị trong Client Portal, đồng thời kiểm tra chuỗi không bị cắt khi truyền.
Webhook không được gọi Kiểm tra URL truy cập được từ Internet, đã chọn đúng sự kiện, và tường lửa không chặn. Lịch sử gửi trong Client Portal cho biết PayFS đã thử gửi chưa và nhận được mã phản hồi nào.