Khởi tạo thanh toán từ Backend
API initPayment được gọi trực tiếp từ Order Backend của các PnL đi qua MuleSoft rồi đẩy tới Payment Core (tương tự như các API cancel/refund/confirm trong Quản lý giao dịch thanh toán).
Trước khi gọi initPayment, Order Backend có thể gọi API lấy danh sách phương thức thanh toán đang enable theo terminal: Lấy danh sách phương thức thanh toán (Backend).
Endpoint
Endpoint: POST /api/payments/v1/transactions
Headers
| Header | Required | Mô tả |
|---|---|---|
X-Payment-API-Key | ✅ | API key của merchant/terminal |
X-Request-ID | ✅ | Unique request ID (UUID) để đảm bảo idempotency và request tracing |
X-Timestamp | ✅ | Unix timestamp in seconds |
X-MiniApp-User-ID | ✅* | MiniApp/V-App SDK mode. ID mà VSF cung cấp cho user MiniApp để SuperApp hiển thị & quản trị. PnL tạo transaction cho user nào thì truyền đúng miniAppUserId của user đó lên. Payment sẽ enrich chính xác thông tin v-app-user-id từ IAM. Nếu không enrich được sẽ trả về lỗi |
X-External-User-ID | ✅* | Merchant direct mode. User ID theo hệ thống của Merchant/Order Backend (không phụ thuộc VSF/IAM). Payment Hub sẽ lưu giá trị này để trace/audit và phục vụ đối soát nội bộ; không enrich IAM từ header này |
X-Auth-Audience | ✅ | Audience identifier (Với ứng dụng chạy trên V-app => v-app, Với Vinpearl không đi qua V-app, chạy cho dịch vụ OTA => vinpearl-ota) |
X-MiniApp-Id | ❌ | MiniApp ID trong SuperApp; Payment Hub dùng để xác định ngữ cảnh MiniApp |
client_id | ✅ | Client ID để authenticate với MuleSoft |
client_secret | ✅ | Client secret để authenticate với MuleSoft |
Content-Type | ✅ | application/json |
Ghi chú về định danh user (bắt buộc chọn 1 trong 2):
- MiniApp/V-App SDK integration: gửi
X-MiniApp-User-ID. - Merchant tích hợp trực tiếp (không có MiniApp context): gửi
X-External-User-ID. - Khuyến nghị: Không gửi đồng thời cả 2 header để tránh mập mờ định danh.
Nếu không gửi X-MiniApp-User-ID thì giao dịch sẽ không được enrich v-app-user-id từ IAM (và vì vậy không thể gắn với user MiniApp để SuperApp hiển thị/ quản trị theo user). Merchant vẫn có thể truy vấn/đối soát giao dịch theo orderId, referenceId, transactionId và X-Payment-API-Key.
- Bắt buộc gửi header
X-Request-ID(UUID) cho mỗi request để đảm bảo idempotency và request tracing - Bắt buộc gửi header
X-Timestamp(Unix timestamp in seconds) cho validation và security - API có cơ chế kiểm tra duplicate request dựa trên
X-Request-ID secureHashđược sinh theo công thức tương tự Tạo SecureHash cho đơn hàng nhưng có thêm fieldskipHolding(nếu có) ở cuối chuỗi
Request Body
{
"amount": 300000.0,
"currency": "VND",
"sellerMerchantId": "SELLER_MERCHANT_001",
"providerId": "067d848c-2fc8-4565-985e-f18b78fb9c7e",
"paymentMethodCode": "INTERNATIONAL_CARD",
"userPaymentMethodId": "019b989c-00b0-7a4b-a66e-dec7a4918e57",
"paymentType": "2D",
"referenceId": "REF_123456",
"orderId": "ORDER_001",
"description": "Payment for order: OrderId_1761297780725",
"orderInfo": {
"customerName": "TestCustomer",
"customerEmail": "[email protected]",
"customerPhone": "0123456789",
"orderCreatedAt": 1761297780725,
"items": [
{
"name": "Test Item",
"sku": "SKU_001",
"quantity": 1,
"unitPrice": 100000.0,
"description": "Description for Test Item",
"categoryCode": "CAT_ELECTRONICS",
"categoryName": "Electronics"
}
]
},
"secureHash": "a1b2c3d4e5f6..."
}
| Field | Type | Required | Mô tả |
|---|---|---|---|
amount | number | ✅ | Số tiền thanh toán (integer hoặc float, minor unit) |
currency | string | ✅ | Đơn vị tiền tệ (VD: VND, USD) |
sellerMerchantId | string | ❌ | Mã merchant của seller để lưu thông tin và hạch toán kế toán (mô hình marketplace/multi-seller). Trường này không thay đổi luồng tiền; tiền vẫn chuyển về merchant mặc định theo X-Payment-API-Key |
description | string | ✅ | Mô tả đơn hàng |
orderId | string | ✅ | Mã đơn hàng (unique, chỉ được dùng một lần) |
referenceId | string | ✅ | Mã tham chiếu |
secureHash | string | ✅ | HMAC-SHA256 hash để verify tính toàn vẹn dữ liệu |
paymentMethodCode | string | * | Mã phương thức thanh toán (bắt buộc nếu không có userPaymentMethodId) |
providerId | string | * | ID payment provider (bắt buộc nếu không có userPaymentMethodId) |
userPaymentMethodId | string | ❌ | ID phương thức thanh toán đã liên kết của user |
paymentType | string | ❌ | Loại thanh toán: "2D" hoặc "3D". Mặc định: "3D" |
skipHolding | boolean | ❌ | Bỏ qua holding hay không. Nếu không truyền, sẽ dùng cấu hình trong DB. true = override, bỏ qua holding (tự trừ tiền luôn), false = override, giữ trạng thái HOLDING (cần gọi confirm/cancel) |
businessUnitId | string | ❌ | Mã cơ sở kinh doanh (Business Unit ID) |
branchId | string | ❌ | Mã chi nhánh |
returnURL | string | ❌ | URL để Payment Provider/Payment Hub redirect user về phía frontend sau khi thanh toán xong (không phải API IPN). Xem lưu ý bên dưới. |
providerData | string | ❌ | Dữ liệu bổ sung từ provider |
orderInfo | object | ✅ | Thông tin đơn hàng chi tiết (CustomerOrderInfo) |
orderInfo.customerName | string | ❌ | Tên khách hàng |
orderInfo.customerEmail | string | ❌ | Email khách hàng |
orderInfo.customerPhone | string | ❌ | Số điện thoại khách hàng |
orderInfo.orderCreatedAt | number | ✅ | Unix timestamp (milliseconds) khi đơn hàng được tạo |
orderInfo.items | array | ❌ | Danh sách sản phẩm/dịch vụ trong đơn hàng |
orderInfo.items[].name | string | ❌ | Tên sản phẩm |
orderInfo.items[].sku | string | ❌ | Mã SKU sản phẩm |
orderInfo.items[].quantity | number | ✅ | Số lượng |
orderInfo.items[].unitPrice | number | ✅ | Giá đơn vị |
orderInfo.items[].description | string | ❌ | Mô tả sản phẩm |
orderInfo.items[].categoryCode | string | ❌ | Mã danh mục/ngành hàng (optional) |
orderInfo.items[].categoryName | string | ❌ | Tên danh mục/ngành hàng (optional) |
Lưu ý về returnURL:
Khi sử dụng returnURL (thường dùng cho luồng web), khuyến nghị Merchant embed thông tin định danh đơn hàng vào path thay vì query string, ví dụ:
https://merchant.example.com/payment/result/ORDER_001/REF_123456
Lý do: mỗi Payment Provider (OnePay, VNPAY, v.v.) sẽ append các param riêng của họ vào returnURL khi redirect — tên và cấu trúc param khác nhau theo từng provider. Nếu returnURL đã có sẵn query string (?orderId=...), một số provider sẽ append thêm ? thay vì &, dẫn đến URL không hợp lệ. Dùng path tránh được vấn đề này và đảm bảo Merchant frontend luôn đọc được thông tin định danh đơn hàng nhất quán, không phụ thuộc provider.
Sau khi provider redirect về, Merchant frontend đọc orderId/referenceId từ path và gọi API Payment Hub để lấy kết quả hiển thị cho user:
GET /api/payments/v1/transactions?orderId={orderId}&referenceId={referenceId}
returnURL chỉ dùng để redirect user về UI — không phải kênh xác nhận kết quả thanh toán. Các param provider trả về trên URL không được dùng làm căn cứ xác nhận giao dịch thành công vì tên field khác nhau theo từng provider và có thể bị giả mạo. Merchant bắt buộc phải xử lý IPN để cập nhật trạng thái đơn hàng chính xác (xem Xử lý kết quả thanh toán (IPN)).
Lưu ý về phương thức thanh toán:
- Bắt buộc: Phải truyền một trong hai cách:
- Cặp
paymentMethodCode+providerId(thanh toán mới) userPaymentMethodId(thanh toán bằng thẻ đã liên kết)
- Cặp
- Ưu tiên: Nếu truyền cả hai, hệ thống sẽ ưu tiên sử dụng
userPaymentMethodId
Lưu ý về secureHash:
- Sinh theo công thức:
HMAC-SHA-256(secretKey, orderId|referenceId|amount|currency|createdAt|branchId|businessUnitId|sellerMerchantId|paymentType|skipHolding) - Lưu ý Amount Hash:
amountđược lấy phần nguyên (integer part string) để hash. - Công thức cơ bản giống với Tạo SecureHash cho đơn hàng nhưng có thêm field
skipHolding(nếu có) append vào cuối chuỗi saupaymentType - Thứ tự append:
orderId|referenceId|amount|currency|createdAt→branchId(nếu có) →businessUnitId(nếu có) →sellerMerchantId(nếu có) →paymentType(nếu có) →skipHolding(nếu có, chỉ khi truyền vào request) sellerMerchantIdlà optional field: chỉ tham gia secureHash khi có truyền vào request- Lý do sắp xếp: gom nhóm field định danh/context (
branchId,businessUnitId,sellerMerchantId) trước, rồi đến field điều khiển luồng thanh toán (paymentType,skipHolding) để chuẩn hóa logic ký và giảm rủi ro implement sai thứ tự giữa các hệ thống
Response
Success Response (200):
{
"code": 0,
"message": "Success",
"data": {
"transaction": {
"id": "019b9b97-e6f8-7eb9-9c29-9b74fd91ee1d",
"referenceId": "Reference_1767841980672768648",
"orderId": "OrderId1767841980672768648",
"amount": 300000,
"currency": "VND",
"status": "PROCESSING",
"description": "Payment for order: OrderId_1761297780725",
"cardType": "2D",
"skipHolding": false,
"expiresAt": "2026-01-08T03:43:02Z",
"createdAt": "2026-01-08T03:13:02Z",
"updatedAt": "2026-01-08T03:13:05Z"
},
"paymentInfo": {
"requiresRedirect": false,
"redirectUrl": "",
"providerCode": "onepay",
"providerId": "067d848c-2fc8-4565-985e-f18b78fb9c7e",
"providerTransaction": "PAY-ECTGEQCTYRRUYZ5E",
"amount": 300000
}
}
}
| Field | Type | Mô tả |
|---|---|---|
code | integer | Mã trạng thái (0 = thành công) |
message | string | Thông báo kết quả |
data.transaction | object | Thông tin transaction |
data.transaction.id | string | ID của transaction được tạo |
data.transaction.referenceId | string | Mã tham chiếu |
data.transaction.orderId | string | ID đơn hàng |
data.transaction.amount | integer | Số tiền thanh toán |
data.transaction.currency | string | Đơn vị tiền tệ |
data.transaction.status | string | Trạng thái transaction (PENDING, PROCESSING, HOLDING, COMPLETED, CANCELLED, etc.) |
data.transaction.description | string | Mô tả đơn hàng |
data.transaction.cardType | string | Loại thẻ thanh toán (2D, 3D) |
data.transaction.skipHolding | boolean | Giá trị skip holding thực tế được áp dụng (từ request hoặc cấu hình trong DB). true = bỏ qua holding, tự động capture ngay sau authorization (tự trừ tiền luôn), false = giữ trạng thái HOLDING (cần gọi confirm/cancel) |
data.transaction.expiresAt | string | Thời gian hết hạn transaction (ISO 8601) |
data.transaction.createdAt | string | Thời gian tạo transaction (ISO 8601) |
data.transaction.updatedAt | string | Thời gian cập nhật transaction (ISO 8601) |
data.paymentInfo | object | Thông tin payment |
data.paymentInfo.requiresRedirect | boolean | Có cần redirect hay không |
data.paymentInfo.redirectUrl | string | URL redirect nếu cần |
data.paymentInfo.providerCode | string | Mã provider (VD: onepay, momo) |
data.paymentInfo.providerId | string | ID của payment provider |
data.paymentInfo.providerTransaction | string | Transaction ID từ provider |
data.paymentInfo.amount | integer | Số tiền thanh toán |
Error Responses:
| HTTP Status | Code | Message | Mô tả |
|---|---|---|---|
| 400 | 4001 | Invalid request | Request không hợp lệ |
| 401 | 4100 | Invalid API key | API Key không hợp lệ |
| 403 | 4200 | Resource does not belong to this user | Transaction không thuộc merchant/terminal |
| 404 | 4302 | User not found | Không thể enrich thông tin từ X-MiniApp-User-ID (không gọi được IAM hoặc user không tồn tại). Chỉ áp dụng khi có gửi X-MiniApp-User-ID. |
| 409 | 4091 | Duplicate referenceId | Reference ID đã được sử dụng |
| 500 | 5000 | Internal server error | Lỗi hệ thống |
Routing qua Mule (Payment Hub)
Quan trọng: API initPayment được gọi trực tiếp từ Order Backend của MiniApp tới Payment Hub thông qua MuleSoft, không được gọi trực tiếp tới Payment Hub.
Internal (PnL)
Kết nối Internal dành cho các PnL trong Vingroup.
Base URL trên Mule (endpoint tới Payment Hub):
- DEV:
{MULE_INTERNAL_BASE_URL}/payment-hub/ - SIT:
{MULE_INTERNAL_BASE_URL}/sit/payment-hub/ - UAT:
{MULE_INTERNAL_BASE_URL}/uat/payment-hub/ - Production:
{MULE_INTERNAL_PROD_URL}/payment-hub/
Lưu ý: Với môi trường Production, các bên kết nối sẽ được team Mule cấp key riêng (client_id và client_secret). Với môi trường DEV/SIT/UAT dùng chung key.
External (Đối tác)
Kết nối External dành cho các đối tác bên ngoài Vingroup. Chỉ hỗ trợ 2 môi trường: Production và UAT.
Base URL trên Mule (endpoint tới Payment Hub):
- UAT:
https://test-api.vingroup.net:7445/payment-hub/ - Production:
https://api-cloud.vingroup.net/payment-hub/
Yêu cầu kết nối External:
- Tất cả các request từ đối tác external cần cung cấp danh sách IPs để whitelist
- Phía Mule sẽ cấp
client_idvàclient_secretriêng cho từng đối tác - Có whitelist và limit access theo endpoint
Examples
cURL Example
curl --location '{MULE_INTERNAL_BASE_URL}/payment-hub/api/payments/v1/transactions' \
--header 'accept: */*' \
--header 'X-Payment-API-Key: ak_uat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
--header 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440000' \
--header 'X-MiniApp-User-ID: 109306626' \
--header 'Content-Type: application/json' \
--header 'X-Auth-Audience: v-app' \
--header 'client_id: YOUR_CLIENT_ID' \
--header 'client_secret: YOUR_CLIENT_SECRET' \
--data '{
"amount": 300000.0,
"currency": "VND",
"sellerMerchantId": "SELLER_MERCHANT_001",
"providerId": "067d848c-2fc8-4565-985e-f18b78fb9c7e",
"paymentMethodCode": "INTERNATIONAL_CARD",
"userPaymentMethodId": "019b989c-00b0-7a4b-a66e-dec7a4918e57",
"paymentType": "2D",
"referenceId": "REF_123456",
"orderId": "ORDER_001",
"description": "Payment for order: OrderId_1761297780725",
"orderInfo": {
"customerName": "TestCustomer",
"customerEmail": "[email protected]",
"customerPhone": "0123456789",
"orderCreatedAt": 1761297780725,
"items": [
{
"name": "Test Item",
"sku": "SKU_001",
"quantity": 1,
"unitPrice": 100000.0,
"description": "Description for Test Item",
"categoryCode": "CAT_ELECTRONICS",
"categoryName": "Electronics"
}
]
},
"secureHash": "a1b2c3d4e5f6..."
}'
cURL Example (Merchant direct mode)
curl --location '{MULE_INTERNAL_BASE_URL}/payment-hub/api/payments/v1/transactions' \
--header 'accept: */*' \
--header 'X-Payment-API-Key: ak_uat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
--header 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440000' \
--header 'X-External-User-ID: merchant_user_123' \
--header 'Content-Type: application/json' \
--header 'X-Auth-Audience: v-app' \
--header 'client_id: YOUR_CLIENT_ID' \
--header 'client_secret: YOUR_CLIENT_SECRET' \
--data '{
"amount": 300000.0,
"currency": "VND",
"sellerMerchantId": "SELLER_MERCHANT_001",
"providerId": "067d848c-2fc8-4565-985e-f18b78fb9c7e",
"paymentMethodCode": "INTERNATIONAL_CARD",
"paymentType": "2D",
"referenceId": "REF_123456",
"orderId": "ORDER_001",
"description": "Payment for order: OrderId_1761297780725",
"orderInfo": {
"customerName": "TestCustomer",
"customerEmail": "[email protected]",
"customerPhone": "0123456789",
"orderCreatedAt": 1761297780725
},
"secureHash": "a1b2c3d4e5f6..."
}'
JavaScript Example
// Example: Init payment from backend
const initPayment = async (paymentData, secretKey, paymentApiKey, userId, clientId, clientSecret) => {
// Generate timestamp (Unix timestamp in seconds)
const timestamp = Math.floor(Date.now() / 1000)
// Generate secureHash (same formula as create order)
// Base: orderId|referenceId|amount|currency|createdAt
// Append branchId if exists, then businessUnitId if exists, then sellerMerchantId if exists,
// then paymentType if exists, then skipHolding if exists
let rawDataString = `${paymentData.orderId}|${paymentData.referenceId}|${paymentData.amount}|${paymentData.currency}|${paymentData.orderInfo.orderCreatedAt}`
if (paymentData.branchId) {
rawDataString += `|${paymentData.branchId}`
}
if (paymentData.businessUnitId) {
rawDataString += `|${paymentData.businessUnitId}`
}
if (paymentData.sellerMerchantId) {
rawDataString += `|${paymentData.sellerMerchantId}`
}
if (paymentData.paymentType) {
rawDataString += `|${paymentData.paymentType}`
}
if (paymentData.skipHolding !== undefined) {
rawDataString += `|${paymentData.skipHolding}`
}
const secureHash = await generateSecureHash(rawDataString, secretKey)
// Generate unique request ID for idempotency
const requestId = crypto.randomUUID()
const response = await fetch('{MULE_INTERNAL_BASE_URL}/payment-hub/api/payments/v1/transactions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Payment-API-Key': paymentApiKey,
'X-Request-ID': requestId,
'X-Timestamp': timestamp.toString(),
'X-MiniApp-User-ID': userId,
'X-Auth-Audience': 'v-app',
'client_id': clientId,
'client_secret': clientSecret,
},
body: JSON.stringify({
...paymentData,
secureHash,
}),
})
return response.json()
}
// Helper function to generate secureHash (HMAC-SHA-256)
async function generateSecureHash(data, hashKey) {
const encoder = new TextEncoder()
const keyData = encoder.encode(hashKey)
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
)
const message = encoder.encode(data)
const signature = await crypto.subtle.sign('HMAC', cryptoKey, message)
const hashArray = Array.from(new Uint8Array(signature))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
Liên kết liên quan
- Tích hợp thanh toán - Luồng thanh toán tổng quan từ V-App
- Tạo SecureHash cho đơn hàng - Hướng dẫn chi tiết cách sinh mã secureHash
- Quản lý giao dịch thanh toán - Các API cancel, refund, confirm
- Xử lý kết quả thanh toán (IPN) - Nhận và xử lý IPN message sau khi thanh toán thành công