VNVN Webhook Integration Guide
Hướng dẫn tích hợp webhook cho các sự kiện thanh toán VNVN (nạp tiền / chuyển tiền VND).
Mục lục#
Tổng quan#
Hệ thống webhook VNVN cho phép bạn nhận thông báo HTTP realtime khi có giao dịch VND xảy ra trên tài khoản, bao gồm:| Event | Mô tả |
|---|
VNVN_PAYMENT_IN | User nhận tiền vào tài khoản (nạp tiền từ ngân hàng) |
VNVN_PAYMENT_OUT | User chuyển tiền ra (chuyển khoản tới ngân hàng) |
Webhook được gửi dưới dạng HTTP POST tới URL HTTPS mà bạn đã đăng ký.
Webhook chỉ được gửi sau khi nghiệp vụ hoàn tất
Với VNVN_PAYMENT_IN: chỉ gửi khi giao dịch không phải idempotent (tránh gửi trùng)
Với VNVN_PAYMENT_OUT: gửi cả khi SUCCESS lẫn FAILED
Event Types#
VNVN_PAYMENT_IN — Nạp tiền#
Được gửi khi user nhận tiền vào tài khoản thành công (nạp tiền từ ngân hàng qua).Balance đã được cộng thành công
Notification đã gửi cho user
Giao dịch không phải là idempotent (không gửi trùng)
{
"transaction_id": "cm3abc123def",
"amount": 1000000,
"fee_amount": 10000,
"final_amount": 990000,
"balance_after": 5000000,
"status": "SUCCESS",
"bank_code": "Vietcombank",
"bank_name": "NGAN HANG TMCP NGOAI THUONG VIET NAM",
"bank_account_number": "0123456789",
"description": "Thanh toan don hang #123",
"timestamp": "2026-03-09T10:30:00.000Z"
}
| Field | Type | Bắt buộc | Mô tả |
|---|
transaction_id | string | ✅ | ID giao dịch nội bộ (UserTransaction) |
amount | number | ✅ | Số tiền giao dịch gốc (VND) |
fee_amount | number | ✅ | Phí giao dịch đã tính (VND) |
final_amount | number | ✅ | Số tiền thực nhận = amount - fee_amount |
balance_after | number | ✅ | Số dư tài khoản sau giao dịch (VND) |
status | string | ✅ | Luôn là "SUCCESS" |
bank_code | string | ❌ | Mã ngân hàng người chuyển |
bank_name | string | ❌ | Tên đầy đủ ngân hàng |
bank_account_number | string | ❌ | Số tài khoản người chuyển |
description | string | ❌ | Nội dung chuyển khoản |
timestamp | string | ✅ | Thời điểm sự kiện (ISO 8601) |
VNVN_PAYMENT_OUT — Chuyển tiền#
Được gửi khi user thực hiện chuyển khoản tới ngân hàng. Webhook được gửi cả khi thành công và thất bại.Trường hợp SUCCESS#
PayPay API xác nhận chuyển tiền thành công
UserTransaction status = SUCCESS
{
"transaction_id": "cm3def456ghi",
"amount": 500000,
"fee_amount": 0,
"status": "SUCCESS",
"bank_code": "Vietcombank",
"bank_name": "NGAN HANG TMCP NGOAI THUONG VIET NAM",
"bank_account_number": "0987654321",
"description": "Chuyen tien cho A",
"timestamp": "2026-03-09T10:35:00.000Z"
}
Trường hợp FAILED#
ITax API trả về lỗi hoặc exception xảy ra
UserTransaction status = FAILED
Balance đã unlock (không trừ tiền)
{
"transaction_id": "cm3ghi789jkl",
"amount": 500000,
"status": "FAILED",
"error_message": "Insufficient funds in merchant account",
"bank_code": "Vietcombank",
"bank_name": "NGAN HANG TMCP NGOAI THUONG VIET NAM",
"bank_account_number": "0987654321",
"description": "Chuyen tien cho A",
"timestamp": "2026-03-09T10:40:00.000Z"
}
Chi tiết fields (VNVN_PAYMENT_OUT):| Field | Type | Bắt buộc | Có ở SUCCESS | Có ở FAILED | Mô tả |
|---|
transaction_id | string | ❌ | ✅ | ⚠️ Có thể null | ID giao dịch nội bộ |
amount | number | ✅ | ✅ | ✅ | Số tiền chuyển (VND) |
fee_amount | number | ❌ | ✅ | ❌ | Phí giao dịch (VND) |
status | string | ✅ | "SUCCESS" | "FAILED" | Trạng thái giao dịch |
error_message | string | ❌ | ❌ | ✅ | Mô tả lỗi (chỉ khi FAILED) |
bank_code | string | ❌ | ✅ | ✅ | Mã ngân hàng người nhận |
bank_name | string | ❌ | ✅ | ✅ | Tên ngân hàng người nhận |
bank_account_number | string | ❌ | ✅ | ✅ | Số tài khoản người nhận |
description | string | ❌ | ✅ | ✅ | Nội dung chuyển khoản |
timestamp | string | ✅ | ✅ | ✅ | Thời điểm sự kiện (ISO 8601) |
Cấu trúc HTTP Request#
Hệ thống gửi HTTP POST tới URL webhook của bạn với body JSON.Content-Type: application/json
X-Webhook-Topic: vnvn.payment-in.status-updated
X-HKPay-Event-Signature: base64_encoded_ed25519_signature
X-Webhook-Id: cm3abc123def456
X-Webhook-Timestamp: 2026-03-09T10:30:01.000Z
| Header | Mô tả |
|---|
Content-Type | Luôn là application/json |
X-Webhook-Topic | Topic của event (xem bảng topic bên dưới) |
X-HKPay-Event-Signature | Chữ ký Ed25519 (base64) trên toàn bộ request body |
X-Webhook-Id | ID unique của delivery |
X-Webhook-Timestamp | Thời điểm gửi webhook (ISO 8601) |
Payload#
{
"id": "cm3abc123def456",
"topic": "vnvn.payment-in.status-updated",
"ts": "2026-03-09T10:30:01.000Z",
"payload": {
"transaction_id": "cm3abc123def",
"amount": 1000000,
"fee_amount": 10000,
"final_amount": 990000,
"balance_after": 5000000,
"status": "SUCCESS",
"bank_code": "Vietcombank",
"bank_name": "NGAN HANG TMCP NGOAI THUONG VIET NAM",
"bank_account_number": "0123456789",
"description": "Thanh toan don hang #123",
"timestamp": "2026-03-09T10:30:00.000Z"
}
}
{
"id": "cm3def456ghi789",
"topic": "vnvn.payment-out.status-updated",
"ts": "2026-03-09T10:35:01.000Z",
"payload": {
"transaction_id": "cm3def456ghi",
"amount": 500000,
"fee_amount": 0,
"status": "SUCCESS",
"bank_code": "Vietcombank",
"bank_name": "NGAN HANG TMCP NGOAI THUONG VIET NAM",
"bank_account_number": "0987654321",
"description": "Chuyen tien cho A",
"timestamp": "2026-03-09T10:35:00.000Z"
}
}
| Field | Type | Mô tả |
|---|
id | string | ID unique của delivery (dùng để tra cứu, tránh xử lý trùng) |
topic | string | Topic theo event type (xem bảng bên dưới) |
ts | string | Thời điểm gửi webhook (ISO 8601) |
payload | object | Dữ liệu chi tiết theo event type (xem bảng ở trên) |
| Event Type | Topic |
|---|
VNVN_PAYMENT_IN | vnvn.payment-in.status-updated |
VNVN_PAYMENT_OUT | vnvn.payment-out.status-updated |
Phân biệt PAYMENT_IN và PAYMENT_OUT: Dựa vào topic:vnvn.payment-in.status-updated → Nạp tiền (PAYMENT_IN)
vnvn.payment-out.status-updated → Chuyển tiền (PAYMENT_OUT)
Xử lý webhook#
Yêu cầu endpoint của bạn#
1.
HTTPS bắt buộc — URL phải sử dụng giao thức HTTPS
2.
Trả về HTTP 2xx — Hệ thống coi response 200-299 là thành công
3.
Xử lý nhanh — Nên trả response trong vòng 10 giây, xử lý nghiệp vụ nặng nên đẩy vào queue riêng
4.
Idempotent — Sử dụng id (delivery ID) để tránh xử lý trùng lặp
Ví dụ xử lý webhook (Node.js / Express)#
Retry & Delivery#
Hệ thống tự động retry khi webhook gửi thất bại (HTTP status không phải 2xx hoặc timeout).Chiến lược retry#
| Lần thử | Delay | Thời điểm retry |
|---|
| Lần 1 | Ngay lập tức | T + 0 |
| Lần 2 | 30 giây | T + 30s |
| Lần 3 | 2 phút | T + 2m30s |
| Lần 4 | 10 phút | T + 12m30s |
| Lần 5 | 1 giờ | T + 1h12m30s |
Tối đa 5 lần thử (1 lần gốc + 4 lần retry)
Nếu sau 5 lần vẫn thất bại → delivery status = FAILED
Delay tăng theo exponential backoff: 30s → 2m → 10m → 1h → 4h
Bảo mật#
Các lưu ý quan trọng#
1.
Chỉ HTTPS — Hệ thống không gửi webhook tới URL HTTP không mã hóa
2.
Xác minh chữ ký — Luôn verify X-HKPay-Event-Signature để đảm bảo webhook đến từ HKS
3.
Idempotency — Luôn kiểm tra id trước khi xử lý để tránh xử lý trùng
4.
Timeout — Hệ thống chờ response tối đa 10 giây, sau đó coi như thất bại và retry
5.
Không chứa dữ liệu nhạy cảm — Payload webhook không bao gồm mật khẩu, secret key, hay thông tin nhạy cảm
Xác minh Webhook (Verify Signature)#
Để đảm bảo tính xác thực của webhook, hãy xác minh chữ ký trong header X-HKPay-Event-Signature bằng Webhook Checksum Key (do HKS cung cấp).Chữ ký được tạo bằng thuật toán Ed25519 dựa trên toàn bộ nội dung request body.Lưu ý: Webhook Checksum Key là Ed25519 public key (base64 encoded). Bạn nhận key này khi đăng ký webhook endpoint.
Go Example#
Java Example#
Node.js Example#
Ví dụ sử dụng trong Express#
FAQ#
Q: Webhook có gửi khi giao dịch bị trùng (idempotent) không?#
A: Không. Với VNVN_PAYMENT_IN, nếu giao dịch đã được xử lý trước đó (idempotent response), webhook sẽ không được gửi lại.Q: VNVN_PAYMENT_OUT có gửi khi thất bại không?#
A: Có. Webhook được gửi cả khi SUCCESS và FAILED. Bạn cần kiểm tra field status trong payload để xử lý phù hợp.Q: Làm sao phân biệt PAYMENT_IN và PAYMENT_OUT?#
A: Mỗi event type có topic riêng biệt, không cần phân biệt qua payload:vnvn.payment-in.status-updated → Nạp tiền (PAYMENT_IN)
vnvn.payment-out.status-updated → Chuyển tiền (PAYMENT_OUT)
Q: Nếu endpoint bị tắt (isActive = false) thì sao?#
A: Hệ thống sẽ bỏ qua và không tạo delivery record. Khi bật lại, chỉ nhận webhook cho các giao dịch mới, không gửi lại các giao dịch đã bị bỏ qua.Q: Webhook có gửi theo thứ tự không?#
A: Hệ thống cố gắng gửi theo thứ tự, nhưng do retry mechanism, thứ tự có thể không đảm bảo 100%. Sử dụng timestamp trong payload để sắp xếp. Modified at 2026-05-12 16:30:10