Quản lý giao dịch thanh toán
Sau khi khởi tạo giao dịch thanh toán, Order Backend của Mini App có thể thực hiện các thao tác quản lý:
Operations cơ bản
- Cancel: Hủy giao dịch chưa được capture/settle
- Refund: Hoàn tiền cho giao dịch đã hoàn thành
Two-Phase Payment (Authorization & Capture)
Một số Payment Provider hỗ trợ cơ chế hold tiền tạm thời trước khi thực sự trừ tiền. Với cơ chế này, Order Backend có thêm operation:
- Confirm: Xác nhận capture tiền đang hold (chuyển từ
HOLDING→COMPLETED)
Nếu không confirm, giao dịch sẽ tự động hủy sau thời gian timeout (thường 7-15 ngày).
Luồng Cancel và Refund
Quy trình xử lý
- User → Mini App: Người dùng bấm hủy đơn trên Mini App
- Mini App → Order Backend: Gửi request hủy đơn (orderId, user info)
- Order Backend: Kiểm tra trạng thái đơn và thanh toán
- Nếu chưa thanh toán/authorized → gọi
PUT /api/payments/v1/transactions/cancel - Nếu đã capture/settle → gọi
POST /api/payments/v1/transactions/refund - Sinh mã
secureHashđể gửi cùng payload request đến Payment Hub
- Nếu chưa thanh toán/authorized → gọi
- Payment Hub:
- Verify thông tin
secureHashtrước khi xử lý - Với cancel: Cập nhật trạng thái
CANCELLED, trả kết quả đồng bộ cho Order Backend - Với refund: Gọi Payment Provider để thực hiện refund, trả kết quả nhận yêu cầu (
PENDING/PROCESSING)
- Verify thông tin
- Payment Provider: Xử lý refund, gửi callback kết quả cuối cùng về Payment Hub
- Notify kết quả: Payment Hub → Order Backend → Mini App → User (giống luồng notify kết quả thanh toán ban đầu)
API Endpoints
| Endpoint | Method | Mô tả |
|---|---|---|
/api/payments/v1/transactions/cancel | PUT | Hủy giao dịch chưa được capture |
/api/payments/v1/transactions/refund | POST | Hoàn tiền giao dịch đã capture/settle |
/api/payments/v1/transactions/confirm | PUT | Xác nhận capture tiền đang hold (Two-Phase Payment) |
- Tất cả các API đều yêu cầu
secureHashđược sinh từ Order Backend - Payment Hub sẽ verify
secureHashtrước khi xử lý request - 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 - Các API có cơ chế kiểm tra duplicate request dựa trên
X-Request-ID
Two-Phase Payment (Authorization & Capture)
Một số Payment Provider (như Momo, ...) cung cấp cơ chế HOLD tiền, cho phép phong tỏa số tiền tạm thời trước khi thực sự trừ tiền khỏi tài khoản khách hàng.
Phase 1: Authorization (Hold tiền)
- User Checkout: Thực hiện như flow init transaction bình thường
- Order Backend: Tạo đơn hàng, gửi lại thông tin cho MiniApp/Payment SDK
- Payment SDK: Init giao dịch thanh toán qua Payment Hub
- Payment Hub: Gọi Payment Provider (VD: Momo) để hold request
- Payment Provider: Trả về status
HOLDING(tiền bị phong tỏa, chưa trừ) - Order Backend: Nhận callback với status =
HOLDING
Phase 2: Confirm hoặc Cancel
Trường hợp 1: Đơn hàng hoàn thành ✅
Khi đơn hàng được giao/hoàn thành:
- Order Backend gọi
PUT /api/payments/v1/transactions/confirmđến Payment Hub - Payment Hub gọi Payment Provider (Momo) CONFIRM/CAPTURE API
- Status chuyển:
HOLDING→COMPLETED - Tiền được trừ khỏi tài khoản customer
// Example: Confirm payment với transactionId
const confirmPayment = async (transactionId, secretKey) => {
// Generate timestamp (Unix timestamp in seconds)
const timestamp = Math.floor(Date.now() / 1000)
// Generate secureHash
const rawDataString = `${transactionId}|${timestamp}`
const secureHash = await generateSecureHash(rawDataString, secretKey)
// Generate unique request ID for idempotency
const requestId = crypto.randomUUID()
const response = await fetch('/api/payments/v1/transactions/confirm', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Payment-API-Key': 'your_payment_api_key',
'X-Request-ID': requestId,
'X-Timestamp': timestamp.toString(),
},
body: JSON.stringify({
transactionId,
secureHash,
}),
})
return response.json()
}
// Example: Confirm payment với orderId và referenceId
const confirmPaymentWithOrderRef = async (orderId, referenceId, secretKey) => {
// Generate timestamp (Unix timestamp in seconds)
const timestamp = Math.floor(Date.now() / 1000)
// Generate secureHash
const rawDataString = `${orderId}|${referenceId}|${timestamp}`
const secureHash = await generateSecureHash(rawDataString, secretKey)
// Generate unique request ID for idempotency
const requestId = crypto.randomUUID()
const response = await fetch('/api/payments/v1/transactions/confirm', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Payment-API-Key': 'your_payment_api_key',
'X-Request-ID': requestId,
'X-Timestamp': timestamp.toString(),
},
body: JSON.stringify({
orderId,
referenceId,
secureHash,
}),
})
return response.json()
}
Trường hợp 2: Đơn hàng thất bại/hủy ❌
Khi đơn hàng bị cancel/hết hàng/giao thất bại:
- Order Backend gọi
PUT /api/payments/v1/transactions/cancelđến Payment Hub - Payment Hub gọi Payment Provider (Momo) CANCEL API
- Status chuyển:
HOLDING→CANCELLED - Tiền được hoàn về cho customer
// Example: Cancel payment với transactionId
const cancelPayment = async (transactionId, reason, requestedBy, secretKey) => {
// Generate timestamp (Unix timestamp in seconds)
const timestamp = Math.floor(Date.now() / 1000)
// Generate secureHash
const rawDataString = `${transactionId}|${timestamp}`
const secureHash = await generateSecureHash(rawDataString, secretKey)
// Generate unique request ID for idempotency
const requestId = crypto.randomUUID()
const response = await fetch('/api/payments/v1/transactions/cancel', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Payment-API-Key': 'your_payment_api_key',
'X-Request-ID': requestId,
'X-Timestamp': timestamp.toString(),
},
body: JSON.stringify({
transactionId,
reason,
requestedBy,
secureHash,
}),
})
return response.json()
}
// Example: Cancel payment với orderId và referenceId
const cancelPaymentWithOrderRef = async (orderId, referenceId, reason, requestedBy, secretKey) => {
// Generate timestamp (Unix timestamp in seconds)
const timestamp = Math.floor(Date.now() / 1000)
// Generate secureHash
const rawDataString = `${orderId}|${referenceId}|${timestamp}`
const secureHash = await generateSecureHash(rawDataString, secretKey)
// Generate unique request ID for idempotency
const requestId = crypto.randomUUID()
const response = await fetch('/api/payments/v1/transactions/cancel', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Payment-API-Key': 'your_payment_api_key',
'X-Request-ID': requestId,
'X-Timestamp': timestamp.toString(),
},
body: JSON.stringify({
orderId,
referenceId,
reason,
requestedBy,
secureHash,
}),
})
return response.json()
}
Trường hợp 3: Timeout ⏱️
Khi quá thời gian hold (thường 7-15 ngày tùy Payment Provider):
- Payment Provider tự động CANCEL
- Tiền tự động hoàn về cho customer
- Status chuyển:
HOLDING→CANCELLED(hoặcTIMEOUT)
Transition Status của Transaction
PENDING → PROCESSING → HOLDING → COMPLETED
↘ CANCELLED
↘ TIMEOUT
Cấu hình Auto-Capture
Merchant-Provider Configuration
Payment Hub hỗ trợ cấu hình autoCapture theo từng Merchant-Provider và phương thức thanh toán:
1. Provider Payment Methods Configuration
Bảng provider_payment_methods xác định phương thức thanh toán nào của Provider hỗ trợ nghiệp vụ HOLDING:
| Provider | Payment Method | Support Holding |
|---|---|---|
| MOMO | E_WALLET | ✅ Yes |
2. Payment Method Configs
Hệ thống Payment cho phép các Merchant/Terminal ON/OFF logic autoCapture:
- ON: Tự động capture ngay sau khi authorization thành công (bỏ qua phase HOLDING)
- OFF: Giữ trạng thái HOLDING, chờ Order Backend gọi confirm/cancel
Khi bật autoCapture, hệ thống sẽ kiểm tra:
- Phương thức thanh toán tương ứng của Provider có hỗ trợ nghiệp vụ HOLDING không
- Nếu không hỗ trợ, request sẽ bị reject
Timeout Handling
Cron Job Monitoring
Payment Hub chạy cron job định kỳ để kiểm tra các giao dịch có trạng thái HOLDING quá lâu:
- Kiểm tra: Các giao dịch HOLDING quá thời gian xử lý (configurable, mặc định 7 ngày)
- Hành động: Tự động gọi cancel nếu quá timeout
- Đồng bộ: Cập nhật trạng thái đơn hàng ở Order Backend qua IPN callback
- Cần đồng bộ chặt chẽ giữa trạng thái giao dịch (Payment Hub) và trạng thái đơn hàng (Order Backend)
- Order Backend nên implement retry logic khi gọi confirm/cancel API
- Luôn kiểm tra trạng thái hiện tại trước khi gọi confirm/cancel
Sinh SecureHash cho Operation APIs
Công thức và Format
Sử dụng HMAC-SHA-256 với format data tương ứng cho từng API:
| API | Raw Data String Format |
|---|---|
| Cancel | Nếu có transactionId: transactionId + '|' + timestampNếu có orderId và referenceId (và không có transactionId): orderId + '|' + referenceId + '|' + timestamp |
| Confirm | Nếu có transactionId: transactionId + '|' + timestampNếu có orderId và referenceId (và không có transactionId): orderId + '|' + referenceId + '|' + timestamp |
| Refund | Cơ bản: ... + refundType + '|' + timestamp. Nếu có refundVpoint (PARTIAL từ Payment Session): thêm refundVpoint trước timestamp: ... + refundType + '|' + refundVpoint + '|' + timestamp.• Với transactionId: transactionId + '|' + amount + '|' + refundReferenceId + '|' + refundType [+ '|' + refundVpoint nếu có] + '|' + timestamp• Với orderId + referenceId: orderId + '|' + referenceId + '|' + amount + '|' + refundReferenceId + '|' + refundType [+ '|' + refundVpoint nếu có] + '|' + timestamp |
Sample Code
async function getExpectedHash(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('')
}
// Example for Cancel/Confirm với transactionId
const cancelDataWithTransactionId = {
transactionId: '515e50c8-6040-46ee-8ae9-0f710faa7fd5',
timestamp: '123',
}
getExpectedHash(
`${cancelDataWithTransactionId.transactionId}|${cancelDataWithTransactionId.timestamp}`,
'sk_dev_xx7ca9hvyneral068d06mr2l5tb3',
).then(h => console.log('Cancel/Confirm Hash (with transactionId) =', h))
// Example for Cancel/Confirm với orderId và referenceId
const cancelDataWithOrderRef = {
orderId: 'ORDER_001',
referenceId: 'REF_123456',
timestamp: '123',
}
getExpectedHash(
`${cancelDataWithOrderRef.orderId}|${cancelDataWithOrderRef.referenceId}|${cancelDataWithOrderRef.timestamp}`,
'sk_dev_xx7ca9hvyneral068d06mr2l5tb3',
).then(h => console.log('Cancel/Confirm Hash (with orderId/referenceId) =', h))
// Example for Refund (FULL - no refundVpoint)
const refundData = {
transactionId: 'bc7c723a-641f-4158-8414-b54611f6f3bf',
amount: '100000',
currency: 'VND',
refundReferenceId: 'REF-123123',
refundType: 'FULL',
timestamp: '1760775890001',
}
const refundStr = `${refundData.transactionId}|${refundData.amount}|${refundData.refundReferenceId}|${refundData.refundType}|${refundData.timestamp}`
getExpectedHash(refundStr, 'sk_dev_xx7ca9hvyneral068d06mr2l5tb3').then(h => console.log('Refund Hash (FULL) =', h))
// Example for Refund (PARTIAL với refundVpoint - Payment Session)
const refundPartialData = {
orderId: 'ORDER_001',
referenceId: 'REF_001',
amount: '50000',
currency: 'VND',
refundReferenceId: 'REF-REFUND-002',
refundType: 'PARTIAL',
refundVpoint: 10000,
timestamp: '1760775890002',
}
const refundPartialStr = `${refundPartialData.orderId}|${refundPartialData.referenceId}|${refundPartialData.amount}|${refundPartialData.refundReferenceId}|${refundPartialData.refundType}|${refundPartialData.refundVpoint}|${refundPartialData.timestamp}`
getExpectedHash(refundPartialStr, 'sk_dev_xx7ca9hvyneral068d06mr2l5tb3').then(h => console.log('Refund Hash (PARTIAL + refundVpoint) =', h))
Lưu ý quan trọng
- Sử dụng HMAC-SHA-256 để tạo chữ ký số
- Format dữ liệu cần ký khác nhau cho mỗi loại operation:
- Cancel/Confirm:
- Nếu có
transactionId:transactionId|timestamp(ưu tiên) - Nếu có
orderIdvàreferenceId(và không cótransactionId):orderId|referenceId|timestamp
- Nếu có
- Refund:
- Nếu có
transactionId:transactionId|amount|refundReferenceId|refundType[|refundVpointnếu có]|timestamp(ưu tiên) - Nếu có
orderIdvàreferenceId:orderId|referenceId|amount|refundReferenceId|refundType[|refundVpointnếu có]|timestamp - refundVpoint: Khi gửi
refundVpoint(PARTIAL refund từ Payment Session), bắt buộc đưa vào chuỗi ký (trướctimestamp) để Payment Hub verify. - Amount:
amountlà số nguyên (integer string), minor unit (VD: 100000 VND). Không chứa ký tự phân cách thập phân.
- Nếu có
- Cancel/Confirm:
- Luôn giữ
secretKeyở backend, không được expose ra client - Kiểm tra kỹ format dữ liệu trước khi ký
- Với Cancel/Confirm: phải truyền một trong hai cách:
orderId+referenceIdHOẶCtransactionId - Ưu tiên: Nếu truyền cả 3 giá trị (
transactionId,orderId,referenceId), hệ thống sẽ ưu tiên sử dụngtransactionIdvà bỏ quaorderIdvàreferenceId
API Reference
1. Cancel Transaction
Hủy giao dịch chưa được capture/settle.
Endpoint: PUT /api/payments/v1/transactions/cancel
Headers:
X-Payment-API-Key: API key của merchantX-Request-ID: Unique request ID (để đảm bảo idempotency)X-Timestamp: Unix timestamp in secondsContent-Type: application/json
Request Body:
Có thể truyền orderId và referenceId (từ init payment) hoặc transactionId:
{
"orderId": "string",
"referenceId": "string",
"transactionId": "string",
"reason": "string",
"requestedBy": "string",
"secureHash": "string"
}
| Field | Type | Required | Mô tả |
|---|---|---|---|
orderId | string | * | ID đơn hàng (bắt buộc nếu không có transactionId) |
referenceId | string | * | Mã tham chiếu (bắt buộc nếu không có transactionId) |
transactionId | string | * | ID của giao dịch (bắt buộc nếu không có orderId và referenceId) |
reason | string | ❌ | Lý do hủy giao dịch |
requestedBy | string | ❌ | Người yêu cầu hủy |
secureHash | string | ✅ | Chữ ký SHA-256 để verify tính toàn vẹn dữ liệu |
Lưu ý:
- Phải truyền một trong hai cách: cặp
orderId+referenceId(từ init payment) HOẶCtransactionId - Ưu tiên: Nếu truyền cả 3 giá trị (
transactionId,orderId,referenceId), hệ thống sẽ ưu tiên sử dụngtransactionIdvà bỏ quaorderIdvàreferenceId
SecureHash Formula (for Cancel/Confirm):
- Nếu có
transactionId:HMAC-SHA-256(secretKey, transactionId + '|' + timestamp)(ưu tiên) - Nếu có
orderIdvàreferenceId(và không cótransactionId):HMAC-SHA-256(secretKey, orderId + '|' + referenceId + '|' + timestamp)
Lưu ý: Nếu truyền cả 3 giá trị, hệ thống sẽ ưu tiên sử dụng transactionId và bỏ qua orderId và referenceId
Success Response (200):
{
"code": 0,
"message": "Thành công"
}
Error Responses:
| HTTP Status | Code | Message | Mô tả |
|---|---|---|---|
| 400 | 4014 | Transaction not available for cancel | Giao dịch không ở trạng thái có thể hủy |
| 401 | 4100 | Invalid API key | API Key không hợp lệ |
| 403 | 4200 | Resource does not belong to this user | Giao dịch không thuộc merchant/terminal |
| 404 | 4301 | Transaction not found | Không tìm thấy giao dịch |
| 500 | 5000 | Internal server error | Lỗi hệ thống |
Example với transactionId:
curl -X 'PUT' \
'https://api.example.com/api/payments/v1/transactions/cancel' \
-H 'X-Payment-API-Key: <your_api_key>' \
-H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440000' \
-H 'X-Timestamp: 1760677974' \
-H 'Content-Type: application/json' \
-d '{
"transactionId": "txn_123456789",
"reason": "Customer requested cancellation",
"requestedBy": "user_001",
"secureHash": "a1b2c3d4e5f6..."
}'
Example với orderId và referenceId:
curl -X 'PUT' \
'https://api.example.com/api/payments/v1/transactions/cancel' \
-H 'X-Payment-API-Key: <your_api_key>' \
-H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440001' \
-H 'X-Timestamp: 1760677974' \
-H 'Content-Type: application/json' \
-d '{
"orderId": "ORDER_001",
"referenceId": "REF_123456",
"reason": "Customer requested cancellation",
"requestedBy": "user_001",
"secureHash": "a1b2c3d4e5f6..."
}'
2. Confirm Transaction
Xác nhận capture tiền đang hold (Two-Phase Payment).
Endpoint: PUT /api/payments/v1/transactions/confirm
Headers:
X-Payment-API-Key: API key của merchantX-Request-ID: Unique request IDX-Timestamp: Unix timestamp in secondsContent-Type: application/json
Request Body:
Có thể truyền orderId và referenceId (từ init payment) hoặc transactionId:
{
"orderId": "string",
"referenceId": "string",
"transactionId": "string",
"secureHash": "string"
}
| Field | Type | Required | Mô tả |
|---|---|---|---|
orderId | string | * | ID đơn hàng (bắt buộc nếu không có transactionId) |
referenceId | string | * | Mã tham chiếu (bắt buộc nếu không có transactionId) |
transactionId | string | * | ID của giao dịch (bắt buộc nếu không có orderId và referenceId) |
secureHash | string | ✅ | Chữ ký SHA-256 để verify tính toàn vẹn dữ liệu |
Lưu ý:
- Phải truyền một trong hai cách: cặp
orderId+referenceId(từ init payment) HOẶCtransactionId - Ưu tiên: Nếu truyền cả 3 giá trị (
transactionId,orderId,referenceId), hệ thống sẽ ưu tiên sử dụngtransactionIdvà bỏ quaorderIdvàreferenceId
SecureHash Formula (for Cancel/Confirm):
- Nếu có
transactionId:HMAC-SHA-256(secretKey, transactionId + '|' + timestamp)(ưu tiên) - Nếu có
orderIdvàreferenceId(và không cótransactionId):HMAC-SHA-256(secretKey, orderId + '|' + referenceId + '|' + timestamp)
Lưu ý: Nếu truyền cả 3 giá trị, hệ thống sẽ ưu tiên sử dụng transactionId và bỏ qua orderId và referenceId
Success Response (200):
{
"code": 0,
"message": "Thành công"
}
Error Responses:
| HTTP Status | Code | Message | Mô tả |
|---|---|---|---|
| 400 | 4015 | Transaction not available for confirm | Giao dịch không ở trạng thái có thể confirm |
| 401 | 4100 | Invalid API key | API Key không hợp lệ |
| 403 | 4200 | Resource does not belong to this user | Giao dịch không thuộc merchant/terminal |
| 404 | 4301 | Transaction not found | Không tìm thấy giao dịch |
| 500 | 5000 | Internal server error | Lỗi hệ thống |
Example với transactionId:
curl -X 'PUT' \
'https://api.example.com/api/payments/v1/transactions/confirm' \
-H 'X-Payment-API-Key: <your_api_key>' \
-H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440001' \
-H 'X-Timestamp: 1760677974' \
-H 'Content-Type: application/json' \
-d '{
"transactionId": "txn_123456789",
"secureHash": "a1b2c3d4e5f6..."
}'
Example với orderId và referenceId:
curl -X 'PUT' \
'https://api.example.com/api/payments/v1/transactions/confirm' \
-H 'X-Payment-API-Key: <your_api_key>' \
-H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440002' \
-H 'X-Timestamp: 1760677974' \
-H 'Content-Type: application/json' \
-d '{
"orderId": "ORDER_001",
"referenceId": "REF_123456",
"secureHash": "a1b2c3d4e5f6..."
}'
3. Refund Transaction
Hoàn tiền cho giao dịch đã hoàn thành.
Endpoint: POST /api/payments/v1/transactions/refund
Refund khi giao dịch từ Payment Session (luồng thanh toán tập trung)
Khi giao dịch gốc được tạo qua luồng thanh toán tập trung (Payment Session – xem Thanh toán tập trung (MiniApp)), đơn hàng có thể đã thanh toán bằng kết hợp tiền (money), VPoint và Promotion (voucher). Refund áp dụng như sau:
| Loại refund | Phạm vi hoàn trả | Cách truyền |
|---|---|---|
| FULL | Hoàn trả đầy đủ phần Promotion (voucher), VPoint và tiền (money). Hệ thống tự tính theo breakdown của session. | Truyền refundType: "FULL". Không cần truyền chi tiết từng phần; amount có thể là tổng số tiền đã thu (hoặc theo quy ước backend). |
| PARTIAL | Chỉ hoàn trả phần VPoint và phần tiền (money); không hoàn Promotion/voucher. | Truyền refundType: "PARTIAL" và bắt buộc truyền đúng số tiền (money) cần hoàn trong amount và số điểm VPoint cần hoàn trong refundVpoint (xem bảng Request Body bên dưới). |
- FULL: Dùng khi hủy/hoàn toàn bộ đơn (hoàn voucher + VPoint + tiền).
- PARTIAL: Dùng khi chỉ hoàn một phần; phải truyền chính xác số tiền và số điểm cần hoàn (để tránh hoàn sai và không hoàn nhầm Promotion).
Headers:
X-Payment-API-Key: API key của merchantX-Request-ID: Unique request IDX-User-ID: User ID thực hiện refundX-Timestamp: Unix timestamp in secondsContent-Type: application/json
Request Body:
{
"amount": 100000,
"transactionId": "string",
"orderId": "string",
"referenceId": "string",
"currency": "VND",
"refundType": "FULL",
"reason": "string",
"requestedBy": "string",
"refundReferenceId": "string",
"refundVpoint": 0,
"secureHash": "string"
}
| Field | Type | Required | Mô tả |
|---|---|---|---|
amount | number | ✅ | Số tiền (money) cần hoàn – integer minor unit (VD: 100000 = 100.000 VND). Với PARTIAL (payment session): bắt buộc truyền đúng số tiền cần hoàn. |
transactionId | string | * | ID của giao dịch cần refund |
orderId | string | * | ID đơn hàng |
referenceId | string | * | Mã tham chiếu đơn hàng |
currency | string | ❌ | Đơn vị tiền tệ (VD: VND, USD). Optional; chỉ dùng đối chiếu với transaction gốc. Refund luôn theo currency của giao dịch gốc. |
refundType | string | ✅ | Loại refund: FULL hoặc PARTIAL |
reason | string | ✅ | Lý do hoàn tiền |
requestedBy | string | ❌ | Người yêu cầu refund |
refundReferenceId | string | ✅ | Reference ID cho refund |
refundVpoint | number | ❌* | Số điểm VPoint cần hoàn. Bắt buộc khi refundType = PARTIAL và giao dịch gốc từ Payment Session (luồng thanh toán tập trung). |
secureHash | string | ✅ | Chữ ký để verify tính toàn vẹn dữ liệu |
Lưu ý:
- Phải truyền một trong hai cách: cặp
orderId+referenceId(từ init payment) HOẶCtransactionId - Ưu tiên: Nếu truyền cả 3 giá trị (
transactionId,orderId,referenceId), hệ thống sẽ ưu tiên sử dụngtransactionIdvà bỏ quaorderIdvàreferenceId
SecureHash Formula:
- Nếu có
transactionId(ưu tiên):- Không có
refundVpoint:transactionId|amount|refundReferenceId|refundType|timestamp - Có
refundVpoint(PARTIAL từ Payment Session):transactionId|amount|refundReferenceId|refundType|refundVpoint|timestamp
- Không có
- Nếu có
orderIdvàreferenceId(và không cótransactionId):- Không có
refundVpoint:orderId|referenceId|amount|refundReferenceId|refundType|timestamp - Có
refundVpoint:orderId|referenceId|amount|refundReferenceId|refundType|refundVpoint|timestamp
- Không có
Chuỗi trên (rawDataString) được ký: HMAC-SHA-256(secretKey, rawDataString).
Lưu ý: amount dùng phần nguyên (integer string, minor unit). Khi gửi refundVpoint thì bắt buộc đưa vào chuỗi ký trước timestamp.
Success Response (200):
{
"code": 0,
"message": "Thành công",
"data": {
"refundId": "refund_123456789",
"remainingRefundableAmount": 2000000
}
}
| 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.refundId | string | ID của refund request |
data.remainingRefundableAmount | integer | Số tiền còn lại có thể refund (minor unit) |
Error Responses:
| HTTP Status | Code | Message | Mô tả |
|---|---|---|---|
| 400 | 4012 | Transaction not available for refund | Giao dịch không ở trạng thái có thể refund |
| 401 | 4100 | Invalid API key | API Key không hợp lệ |
| 403 | 4200 | Resource does not belong to this user | Giao dịch không thuộc merchant/terminal |
| 404 | 4301 | Transaction not found | Không tìm thấy giao dịch |
| 500 | 5000 | Internal server error | Lỗi hệ thống |
Example:
curl -X 'POST' \
'https://api.example.com/api/payments/v1/transactions/refund' \
-H 'X-Payment-API-Key: <your_api_key>' \
-H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440002' \
-H 'X-User-ID: user_001' \
-H 'X-Timestamp: 1760677974' \
-H 'Content-Type: application/json' \
-d '{
"amount": 100000,
"transactionId": "txn_123456789",
"currency": "VND",
"refundType": "full",
"reason": "Customer requested refund",
"requestedBy": "user_001",
"refundReferenceId": "refund_001",
"secureHash": "a1b2c3d4e5f6..."
}
Example với orderId và referenceId:
curl -X 'POST' \
'https://api.example.com/api/payments/v1/transactions/refund' \
-H 'X-Payment-API-Key: <your_api_key>' \
-H 'X-Request-ID: 550e8400-e29b-41d4-a716-446655440003' \
-H 'X-User-ID: user_001' \
-H 'X-Timestamp: 1760677974' \
-H 'Content-Type: application/json' \
-d '{
"amount": 100000,
"orderId": "ORDER_001",
"referenceId": "REF_123456",
"currency": "VND",
"refundType": "full",
"reason": "Customer requested refund",
"requestedBy": "user_001",
"refundReferenceId": "refund_001",
"secureHash": "a1b2c3d4e5f6..."
}'
Routing qua Mule (Payment Hub)
Quan trọng: Các API cancel/confirm/refund và các API lấy transaction đượ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
Quy tắc chung:
- Yêu cầu các header cơ bản:
X-Payment-API-Key,X-Request-ID,X-Timestamp(hoặc theo từng API doc). Thêm header để pass MuleSoft:client_id,client_secret, v.v. - Các request được forward qua Mule tới Payment Hub — đảm bảo token/client/config phù hợp với Mule khi gọi.
Ví dụ gọi API lấy transaction (minh họa header cần thiết):
curl --location 'https://payment-core-api.dev.vsf.services/api/payments/v1/transactions?transactionId=b68405a4-690f-4ab5-8d3b-0b202568be38' \
--header 'accept: */*' \
--header 'X-Payment-API-Key: <YOUR_PAYMENT_API_KEY>' \
--header 'X-Request-ID: 123123123' \
--header 'X-User-ID: 857652265' \
--header 'X-Auth-Audience: v-app' \
--header 'client_id: YOUR_CLIENT_ID' \
--header 'client_secret: YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/json'
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
Quy tắc chung:
- Yêu cầu các header cơ bản:
X-Payment-API-Key,X-Request-ID,X-Timestamp(hoặc theo từng API doc). Thêm header để pass MuleSoft:client_id,client_secret, v.v. - Các request được forward qua Mule tới Payment Hub — đảm bảo token/client/config phù hợp với Mule khi gọi.
Ví dụ gọi API lấy transaction (minh họa header cần thiết):
curl --location 'https://payment-core-api.dev.vsf.services/api/payments/v1/transactions?transactionId=b68405a4-690f-4ab5-8d3b-0b202568be38' \
--header 'accept: */*' \
--header 'X-Payment-API-Key: <YOUR_PAYMENT_API_KEY>' \
--header 'X-Request-ID: 123123123' \
--header 'X-User-ID: 857652265' \
--header 'X-Auth-Audience: v-app' \
--header 'client_id: YOUR_CLIENT_ID' \
--header 'client_secret: YOUR_CLIENT_SECRET' \
--header 'Content-Type: application/json'