Xử lý kết quả thanh toán
Sau khi khách hàng thanh toán trực tuyến trên V-App, chúng tôi thực hiện hàm callback mà đối tác đăng ký khi gọi initPayment. Đồng thời, chúng tôi còn gửi một IPN message đến backend của phía đối tác.
- Luồng bất đồng bộ: Mọi luồng thanh toán (MiniApp chuẩn, thanh toán tập trung / Payment Session, v.v.) đều coi IPN là kênh kết quả chính — backend đối tác nên xử lý cập nhật đơn/giao dịch khi nhận IPN (có thể kết hợp tra cứu API để đối soát); không nên giả định client/SDK trả kết quả đồng bộ thay cho IPN.
- Khi thanh toán thành công, IPN sẽ được gởi ngay lập tức với status
COMPLETED - Khi thanh toán thất bại, IPN sẽ được gởi với trạng thái
FAILEDtrong hai trường hợp sau:- Khách hàng tới màn hình thanh toán và click huỷ thanh toán: IPN sẽ được gởi ngay lập tức
- Khách hàng tới màn hình thanh toán và tắt app hoặc không làm gì cả: IPN sẽ được gởi sau đó 4 tiếng
IPN - Instant Payment Notification
Chúng tôi sẽ sử dụng Postback Url mà đối tác cung cấp lúc đăng ký chức năng thanh toán với V-App để gửi một HTTP request với thông tin như sau:
Thông tin HTTP
| Key | Value |
|---|---|
| Content-Type | application/json |
| Method | POST |
| URL | postback url |
HTTP Request
| Thuộc tính | Kiểu dữ liệu | Mô tả |
|---|---|---|
| orderId | string | ID của order trong hệ thống backend (unique identifier) |
| transactionId | string | ID giao dịch từ Payment Service (unique per transaction) |
| providerTransactionId | string | ID giao dịch từ provider |
| providerInvoiceId | string | ID invoice do provider cung cấp cho PayCollect (có thể null). (OnePay: provider_invoice_id) |
| referenceId | string | ID tham chiếu từ Gateway/Bank (có thể null) |
| transactionType | string | Loại giao dịch: PAYMENT | REFUND | CANCEL | CHARGEBACK |
| amount | number | Số tiền giao dịch (không bao gồm fee), phải > 0 |
| currency | string | Đơn vị tiền tệ theo chuẩn ISO 4217 (VND | USD | EUR | etc) |
| status | string | Trạng thái giao dịch: PENDING | PROCESSING | HOLDING | COMPLETED | FAILED | CANCELLED |
| processedAt | number | Unix timestamp in milliseconds (UTC) |
| paymentMethod | string | Phương thức thanh toán chính (primary) — nguồn quyết định việc hoàn tất giao dịch khi một giao dịch gồm nhiều nguồn tiền (allocations); chi tiết và ví dụ: mục Primary payment method và allocations. Ngoài các giá trị (CREDIT_CARD | DEBIT_CARD | BANK_TRANSFER | E_WALLET | QR_CODE…), luồng có voucher/VPoint có thể dùng thêm giá trị minh hoạ như PROMOTION | VPOINT (tuỳ contract Payment Hub). |
| paymentProvider | string | Hệ thống / cổng gắn với primary (không chỉ PSP thẻ-ví): thanh toán external qua cổng (MOMO | ONEPAY | VNPAY | VIETQR…); khi primary là voucher/promotion có thể là VSF Promotion; khi primary là VPoint có thể là Vinclub. Vẫn có thể null nếu contract không gắn provider cho primary (tuỳ triển khai). |
| bankName | string | Tên ngân hàng phát hành thẻ/tài khoản thanh toán (có thể null) |
| accountHolderName | string | Tên chủ thẻ/chủ tài khoản thanh toán (có thể null) |
| cardNo | string | Số thẻ đã masking (VD: 411111******1111) (có thể null; không trả full PAN) |
| fees | array | Danh sách phí dynamic |
| orderInfo | object | Thông tin khách hàng/đơn hàng (nếu có) |
| secureHash | string | HMAC-SHA256 hash để verify data integrity (64 chars hex) |
| errorCode | string | Mã lỗi (có thể null) |
| errorMessage | string | Thông báo lỗi (có thể null) |
| breakdown | array | (Optional) Mảng chi tiết nhiều nguồn tiền (allocations) trong một giao dịch; thường có ở luồng thanh toán tập trung (Payment Session). Cấu trúc đầy đủ: type, amount, status, referenceId, referenceType, details — xem mục Breakdown trong IPN. |
Lưu ý:
bankName,accountHolderName,cardNothường có khi giao dịch thẻ/tài khoản ngân hàng.- Với một số phương thức như ví điện tử/QR, các field này có thể
null. cardNoluôn là dữ liệu đã masking; Payment Service không trả full số thẻ (PAN).providerInvoiceIdchỉ có ý nghĩa với giao dịch PayCollect; các giao dịch khác có thểnull.- Luồng thanh toán tập trung: Payment Service thường gửi một IPN kết thúc cho cả phiên (thành công / thất bại / huỷ); payload có thể kèm
breakdown. Khi cóbreakdown, ý nghĩaamounttop-level và cách đọc từng dòng nằm ở mục Breakdown trong IPN — khác với luồng MiniApp cổ điển không cóbreakdown. referenceIdở root body IPN (cột HTTP Request) vẫn là tham chiếu theo contract IPN chung (ví dụ Gateway/Bank hoặc tham chiếu đơn, tuỳ triển khai).referenceIdtrong từng phần tửbreakdownlà ID downstream theo từng dòng nguồn tiền — hai tầng khác nhau, đừng gộp nhầm.
Primary payment method và allocations
Một giao dịch có thể tương ứng nhiều nguồn tiền (promotion/voucher, VPoint, tiền qua cổng…). Trong IPN:
breakdown(khi có — thường ở luồng thanh toán tập trung) mô tả từng allocation và số tiền từng dòng; định nghĩa chi tiết field: mục Breakdown trong IPN.paymentMethod(vàpaymentProviderkhi áp dụng) mô tả phương thức thanh toán chính (primary): nguồn quyết định việc giao dịch được coi là complete theo quy ước sau (minh hoạ):
| Case | Thành phần trong đơn / session | paymentMethod (primary) minh hoạ | paymentProvider (primary) minh hoạ | Ghi chú |
|---|---|---|---|---|
| 1 | Chỉ promotion — đơn 100% miễn phí (không VPoint, không cổng) | PROMOTION | VSF Promotion | Thanh toán bằng voucher/khuyến mãi (promotion); không có bước thu tiền qua PSP. |
| 2 | Promotion + VPoint (chưa có tiền qua cổng ngoài) | VPOINT | Vinclub | Promotion chỉ là discount; VPoint là nguồn tiền thực trả nên primary là VPoint / Vinclub. |
| 3 | Promotion + VPoint + thanh toán external (thẻ/ví/QR qua PSP) | CREDIT_CARD (hoặc DEBIT_CARD / E_WALLET / QR_CODE… tuỳ user chọn) | VNPAY | MOMO | ONEPAY… | External là bước cuối quyết định success/fail; paymentProvider là cổng thanh toán tương ứng. |
Tóm lại: paymentMethod / paymentProvider trong payload IPN không liệt kê hết mọi dòng trong breakdown; chúng chỉ định danh primary. paymentProvider không giới hạn cổng thẻ/ví — có thể là VSF Promotion (voucher) hoặc Vinclub (VPoint) khi đó là primary. Muốn phân bổ đầy đủ, đọc breakdown (mục bên dưới).
Breakdown trong IPN
Field breakdown là mảng (optional) mô tả phân bổ nguồn tiền (allocations) khi một giao dịch gồm nhiều thành phần (tiền qua cổng, voucher, VPoint…). Thường xuất hiện cùng luồng thanh toán tập trung (Payment Session): Payment Service gửi một IPN kết thúc cho phiên và có thể kèm breakdown để backend đối tác đối soát từng nguồn.
Mỗi phần tử trong breakdown có dạng:
type: nguồn tiền / dòng breakdown (tiền qua cổng, voucher, VPoint…), ví dụ:PAYMENT,VOUCHER,VPOINT. Khác vớireferenceType(phân loại tham chiếu downstream — xem bảng enum bên dưới).amount: số tiền tương ứng với nguồn đó.status: trạng thái từng item (mỗi phần tử trong mảngbreakdownlà một item) —PENDING|SUCCESS|FAILED|CANCELLED(xem bảngbreakdown[].statusbên dưới). Khác vớistatusgiao dịch ở root IPN (COMPLETED|FAILED| …).referenceId(optional): ID từ hệ thống downstream gắn với dòng breakdown này (PSP, promotion ledger, VPoint…), khi có tham chiếu cần đối soát.referenceType(optional, đi cặpreferenceId): phân loại ý nghĩa củareferenceId. Khuyến nghị đặtreferenceId/referenceTypecùng cấptype/amountthay vì trongdetails: schema thống nhất cho mọitype;detailschỉ chứa field đặc thù từng loại.details(optional): map (object JSON — key-value). Không phải mảng ở root: mỗi key là tên field đặc thù theotype, giá trị có thể là string/number/boolean hoặc mảng (ví dụ keyvoucherCodes→ mảng string). Ví dụPAYMENT:method,provider;VOUCHER: một mã →voucherCode/promotionId(string); nhiều mã →voucherCodes/promotionIds(giá trị là mảng string trong map);VPOINT:points.
Ví dụ payload (rút gọn các field IPN khác):
{
"orderId": "Order_001",
"transactionId": "Txn_001",
"status": "COMPLETED",
"amount": 70000,
"currency": "VND",
"breakdown": [
{
"type": "PAYMENT",
"amount": 70000,
"status": "SUCCESS",
"referenceId": "PSP_TXN_123",
"referenceType": "TRANSACTION",
"details": {
"method": "CARD",
"provider": "ONEPAY"
}
},
{
"type": "VOUCHER",
"amount": 10000,
"status": "SUCCESS",
"referenceId": "PROMO_REV_123",
"referenceType": "RESERVATION",
"details": {
"voucherCodes": ["VOUCHER_2025", "VOUCHER_EXTRA"],
"promotionIds": ["PROMO_001", "PROMO_002"]
}
},
{
"type": "VPOINT",
"amount": 20000,
"status": "SUCCESS",
"referenceId": "PROMO_REV_123",
"referenceType": "REDEMPTION",
"details": {
"points": "20000"
}
}
]
}
Ý nghĩa amount top-level khi có breakdown (minh hoạ):
amount(top-level): số tiền user thực trả qua kênh thanh toán online (tương ứng phầntype: PAYMENTtrongbreakdown), không phải tổng mọi dòng khi có VPoint/voucher.
Theo từng breakdown.type (minh hoạ):
PAYMENT: tiền thu qua cổng thanh toán.status: kết quả xử lý dòng đó.referenceId/referenceType(nếu có): tham chiếu PSP / giao dịch ngoài.details:method,provider.VOUCHER: phần giảm từ voucher/khuyến mãi.status: kết quả áp dụng/hold promotion.referenceId/referenceType: tham chiếu downstream promotion (ví dụ hold).detailsvẫn là map: keyvoucherCodes/promotionIdsvới giá trị là mảng string khi nhiều mã; hoặc keyvoucherCode/promotionId(string) khi một mã. Có thể tách nhiều dòngtype: VOUCHERtrongbreakdown(mỗi dòng một voucher) thay vì gom list trong map — tuỳ contract Payment Hub.VPOINT: phần thanh toán bằng VPoint.status: kết quả trừ/quy đổi điểm.referenceId/referenceType: tham chiếu downstream khi quy đổi/tiêu điểm.details:points(số điểm đã trừ, minh hoạ dạng string).
Field cấp dòng (minh hoạ):
| Field | Bắt buộc | Ý nghĩa |
|---|---|---|
type | Có | Nguồn tiền của dòng: PAYMENT, VOUCHER, VPOINT, … |
amount | Có | Số tiền tương ứng dòng đó. |
status | Có | Trạng thái từng item trong breakdown; giá trị PENDING | SUCCESS | FAILED | CANCELLED — xem bảng breakdown[].status. |
referenceId | Không (khi có tham chiếu downstream) | ID từ hệ thống downstream cho dòng này. |
referenceType | Không (đi cặp referenceId) | Phân loại referenceId; giá trị xem bảng dưới. |
Giá trị status từng item trong breakdown (minh hoạ):
| Giá trị | Ý nghĩa |
|---|---|
PENDING | Item đang xử lý / chờ kết quả. |
SUCCESS | Item đã hoàn tất thành công. |
FAILED | Item thất bại (ví dụ trừ VPoint lỗi, cổng từ chối phần tương ứng). |
CANCELLED | Item đã bị huỷ (user/system). |
details theo từng breakdown.type (minh hoạ): — luôn là map (object); các dòng dưới là key trong map (giá trị có thể scalar hoặc mảng).
| Field (key trong map) | breakdown.type | Ý nghĩa |
|---|---|---|
method | PAYMENT | Phương thức thanh toán (ví dụ CARD). |
provider | PAYMENT | Mã nhà cung cấp thanh toán (ví dụ ONEPAY). |
voucherCode | VOUCHER | (Một voucher) Mã voucher đã áp dụng. |
promotionId | VOUCHER | (Một promotion) Định danh promotion. |
voucherCodes | VOUCHER | (Nhiều voucher) Mảng mã voucher đã áp dụng. |
promotionIds | VOUCHER | (Nhiều promotion) Mảng định danh promotion. |
points | VPOINT | Số điểm đã trừ. |
Giá trị referenceType (minh hoạ):
| Giá trị | Ý nghĩa |
|---|---|
TRANSACTION | Tham chiếu thanh toán bên ngoài (external payment), ví dụ giao dịch trên PSP/cổng. |
RESERVATION | Tham chiếu giữ tài nguyên (hold resource) — đặt chỗ / giữ hạn mức trước khi chốt. |
REDEMPTION | Tham chiếu tiêu thụ tài nguyên (consume resource), ví dụ quy đổi / trừ benefit sau xác nhận. |
Lưu ý về breakdown:
breakdown[].statusdùng bộ giá trịPENDING/SUCCESS/FAILED/CANCELLED— không trùng vớistatusgiao dịch ở root IPN (COMPLETED|FAILED|CANCELLED| …). Root mô tả kết quả tổng thể; mỗi item trongbreakdownmô tả kết quả từng nguồn.- Vẫn cần
breakdown[].type(PAYMENT/VOUCHER/VPOINT…) dù đã córeferenceId/referenceType:typelà nguồn tiền trong đơn;referenceTypechỉ mô tả loại tham chiếu downstream củareferenceId— hai trục khác nhau. breakdown[].typedùngPAYMENT, không dùngTRANSACTIONở đây.TRANSACTION/RESERVATION/REDEMPTIONlà giá trị củareferenceType, không thay thếbreakdown[].type.- Bản triển khai cũ có thể gửi
referenceId/referenceTypelồng trongdetails; contract khuyến nghị đặt hai field ở cấp phần tửbreakdownnhư ví dụ. breakdowncó thể có nhiều phần tử cùngtype(ví dụ nhiềuVOUCHER).- Tổng
amountcác phần tửbreakdownbằng tổng giá trị đơn (tổng các dòngPAYMENT+VOUCHER+VPOINT+ …).amounttop-level là phần user trả qua kênh online; hai giá trị khác nhau khi có VPoint/voucher.
orderInfo Structure
| Thuộc tính | Kiểu dữ liệu | Mô tả |
|---|---|---|
| customerName | string | Tên khách hàng |
| customerEmail | string | Email khách hàng |
| customerPhone | string | Số điện thoại khách hàng |
| orderCreatedAt | number | Unix timestamp (milliseconds) khi order được tạo |
| notes | string | Ghi chú bổ sung |
Fees Array Structure
| Thuộc tính | Kiểu dữ liệu | Mô tả |
|---|---|---|
| type | string | Loại phí: GATEWAY_FEE | PROCESSING_FEE | CURRENCY_CONVERSION | WEEKEND_FEE | PROMOTION_DISCOUNT | etc |
| name | string | Tên phí |
| amount | number | Số tiền phí |
| currency | string | Đơn vị tiền tệ của phí |
| rate | string | Tỷ lệ phí (có thể null) |
| calculation | string | Cách tính phí: PERCENTAGE | FIXED | TIERED | DYNAMIC |
| chargedBy | string | Đơn vị thu phí: GATEWAY | INTERNAL | BANK | GOVERNMENT |
Vi dụ về IPN message request
curl --location --request POST 'https://partner.example.com/payment/ipn' \
--header 'Content-Type: application/json' \
--data-raw '{
"orderId": "Order_001",
"transactionId": "Txn_001",
"providerTransactionId": "provider_txn_001",
"providerInvoiceId": "ONEPAY_INVOICE_001",
"referenceId": "Ref_001",
"transactionType": "PAYMENT",
"amount": 30000,
"currency": "VND",
"status": "COMPLETED",
"processedAt": 1705320600000,
"paymentMethod": "CREDIT_CARD",
"paymentProvider": "VNPAY",
"bankName": "Vietcombank",
"accountHolderName": "NGUYEN VAN A",
"cardNo": "411111******1111",
"fees": [
{
"type": "GATEWAY_FEE",
"name": "VNPay Processing Fee",
"amount": 900,
"currency": "VND",
"rate": "3%",
"calculation": "PERCENTAGE",
"chargedBy": "GATEWAY"
}
],
"orderInfo": {
"customerName": "Nguyen Van A",
"customerEmail": "[email protected]",
"customerPhone": "+84901234567",
"orderCreatedAt": 1760505607000,
"notes": "notes"
},
"secureHash": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
"errorCode": null,
"errorMessage": null
}'
Trạng thái giao dịch
Payment Service sẽ trả về các trạng thái giao dịch sau trong IPN message:
| Trạng thái | Giá trị | Mô tả | Hành động đề xuất |
|---|---|---|---|
| PENDING | PENDING | Đang chờ xử lý | Chờ cập nhật trạng thái tiếp theo. Không coi là thành công hay thất bại |
| PROCESSING | PROCESSING | Đang xử lý thanh toán | Giao dịch đang được xử lý bởi payment gateway. Chờ kết quả cuối cùng |
| HOLDING | HOLDING | Đang giữ tiền, chờ xác nhận | Tiền đã được giữ tạm thời, chờ xác nhận từ merchant hoặc hệ thống |
| COMPLETED | COMPLETED | Thanh toán thành công | Cập nhật đơn hàng thành công, giao hàng cho khách |
| FAILED | FAILED | Thanh toán thất bại | Thông báo cho khách hàng, hủy đơn hàng hoặc yêu cầu thanh toán lại |
| CANCELLED | CANCELLED | Đã hủy giao dịch | Đơn hàng đã bị hủy bởi khách hàng hoặc hệ thống |
- Chỉ coi giao dịch là thành công khi nhận status
COMPLETED - Với status
PENDING,PROCESSING,HOLDING: Chờ IPN tiếp theo, không kết luận kết quả - Với status
FAILEDhoặcCANCELLED: Coi như giao dịch không thành công - Luôn kiểm tra
secureHashtrước khi xử lý bất kỳ status nào - Sử dụng
transactionIdvàprocessedAtđể tránh xử lý trùng lặp
Gửi lại IPN message
Chúng tôi sẽ gửi lại IPN message khi:
- Kết quả trả về có HTTP status codes từ 500 đến 599
- Hơn 15 giây không trả về kết quả
- Xảy lỗi về kết nối mạng
Thời gian gửi lại sẽ tăng theo cấp số nhân: 2s → 4s → 8s → 16s → ...
Số lần retry tối đa được cấu hình khi onboard merchant với Payment Service.
Kiểm tra toàn vẹn dữ liệu
Dữ liệu có thể bị thay đổi trên đường truyền giữa hai hệ thống V-App và đối tác khi giao tiếp qua đường truyền mạng, dẫn đến thông tin giao dịch có thể bị sai lệch, đặc biệt là số tiền thanh toán và kết quả giao dịch.
Giải pháp
- Để đảm bảo thông tin giao dịch chính xác và đầy đủ, đối tác cần KIỂM TRA
secureHashmỗi lần nhận IPN message từ V-App. SecureHash được tính bằng HMAC-SHA-256 với format:HMAC-SHA-256(secretKey, rawDataString(orderId + '|' + transactionId + '|' + transactionType + '|' + amount + '|' + status + '|' + processedAt)). - Sử dụng request
/api/payments/v1/transactionsđể kiểm tra trạng thái giao dịch trước khi cập nhật. - Luôn sử dụng IPN message để xử lý kết quả giao dịch. Khắc phục trường hợp khi thanh toán app V-App bị đóng bất ngờ.
- Vì phụ thuộc vào đường truyền mạng và hệ thống, có khả năng IPN message bị gửi chậm hoặc một message bị gửi nhiều lần. Do đó, đối tác cần dựa vào trường
transactionIdvàprocessedAtnhằm tránh 1 message được xử lý nhiều lần hay xử lý sai thứ tự.
Sample Code để tạo/verify SecureHash (HMAC-SHA-256)
// Example HMAC-SHA-256 using Web Crypto API (browser / Node.js with global crypto)
async function getExpectedHash(data, hashKey) {
const encoder = new TextEncoder()
const keyData = encoder.encode(hashKey)
const message = encoder.encode(data)
// Import secret key for HMAC-SHA-256
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
)
// Compute HMAC-SHA-256 signature
const signature = await crypto.subtle.sign('HMAC', cryptoKey, message)
const hashArray = Array.from(new Uint8Array(signature))
const expectedHash = hashArray
.map(b => b.toString(16).padStart(2, '0'))
.join('')
console.log('data =', data)
console.log('expectedHash =', expectedHash)
return expectedHash
}
// Example test
getExpectedHash(
'25VFX8ME3PQ1|b1ecc589-d029-4214-bd73-80992e0fcd73|PAYMENT|1699000000|COMPLETED|1760669994639',
'sk_dev_js9u6kyzssf1me4znf0vax1fz21i',
).then(h => console.log('Result =', h))
IPN Response
HTTP Status Code
Luôn trả về HTTP Status 200 cho mọi trường hợp xử lý IPN, kể cả thành công hay thất bại.
Response Format
{
"status": "SUCCESS",
"message": "Transaction processed successfully",
"orderId": "Order_001",
"transactionId": "Txn_001",
"processedAt": 1705320600500
}
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | ✅ | Trạng thái xử lý IPN. Domain values: - SUCCESS: Xử lý thành công- FAILED: Xử lý thất bại- INVALID_HASH: SecureHash không hợp lệ- DUPLICATE: IPN đã được xử lý trước đó- ORDER_NOT_FOUND: Không tìm thấy đơn hàng |
| message | string | ❌ | Message mô tả kết quả xử lý (dùng để debug) |
| orderId | string | ❌ | ID của đơn hàng được xử lý |
| transactionId | string | ❌ | ID của giao dịch được xử lý |
| processedAt | number | ❌ | Timestamp khi backend xử lý xong IPN (Unix timestamp in milliseconds) |
Example Responses
Thành công:
{
"status": "SUCCESS",
"message": "Transaction processed successfully",
"orderId": "Order_001",
"transactionId": "Txn_001",
"processedAt": 1705320600500
}
SecureHash không hợp lệ:
{
"status": "INVALID_HASH",
"message": "Invalid secure hash",
"orderId": "Order_001",
"transactionId": "Txn_001",
"processedAt": 1705320600500
}
Trùng lặp IPN:
{
"status": "DUPLICATE",
"message": "IPN already processed",
"orderId": "Order_001",
"transactionId": "Txn_001",
"processedAt": 1705320600500
}
- LUÔN trả về HTTP Status 200 cho mọi trường hợp
- Sử dụng field
statusđể báo kết quả xử lý thực tế - Đảm bảo
processedAtlà timestamp hợp lệ (milliseconds) - Điền đầy đủ
orderIdvàtransactionIdđể tracking - Đặt message rõ ràng để dễ debug khi có vấn đề