Chuyển tới nội dung chính

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ừ HOLDINGCOMPLETED)

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ý

  1. User → Mini App: Người dùng bấm hủy đơn trên Mini App
  2. Mini App → Order Backend: Gửi request hủy đơn (orderId, user info)
  3. 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
  4. Payment Hub:
    • Verify thông tin secureHash trướ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)
  5. Payment Provider: Xử lý refund, gửi callback kết quả cuối cùng về Payment Hub
  6. 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

EndpointMethodMô tả
/api/payments/v1/transactions/cancelPUTHủy giao dịch chưa được capture
/api/payments/v1/transactions/refundPOSTHoàn tiền giao dịch đã capture/settle
/api/payments/v1/transactions/confirmPUTXác nhận capture tiền đang hold (Two-Phase Payment)
Lưu ý
  • Tất cả các API đều yêu cầu secureHash được sinh từ Order Backend
  • Payment Hub sẽ verify secureHash trướ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)

  1. User Checkout: Thực hiện như flow init transaction bình thường
  2. Order Backend: Tạo đơn hàng, gửi lại thông tin cho MiniApp/Payment SDK
  3. Payment SDK: Init giao dịch thanh toán qua Payment Hub
  4. Payment Hub: Gọi Payment Provider (VD: Momo) để hold request
  5. Payment Provider: Trả về status HOLDING (tiền bị phong tỏa, chưa trừ)
  6. 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:

  1. Order Backend gọi PUT /api/payments/v1/transactions/confirm đến Payment Hub
  2. Payment Hub gọi Payment Provider (Momo) CONFIRM/CAPTURE API
  3. Status chuyển: HOLDINGCOMPLETED
  4. 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:

  1. Order Backend gọi PUT /api/payments/v1/transactions/cancel đến Payment Hub
  2. Payment Hub gọi Payment Provider (Momo) CANCEL API
  3. Status chuyển: HOLDINGCANCELLED
  4. 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: HOLDINGCANCELLED (hoặc TIMEOUT)

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:

ProviderPayment MethodSupport Holding
MOMOE_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
Điều kiện bật Auto-Capture

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
Lưu ý quan trọng
  • 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:

APIRaw Data String Format
CancelNếu có transactionId: transactionId + '|' + timestamp
Nếu có orderIdreferenceId (và không có transactionId): orderId + '|' + referenceId + '|' + timestamp
ConfirmNếu có transactionId: transactionId + '|' + timestamp
Nếu có orderIdreferenceId (và không có transactionId): orderId + '|' + referenceId + '|' + timestamp
RefundCơ 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ó orderIdreferenceId (và không có transactionId): orderId|referenceId|timestamp
    • Refund:
      • Nếu có transactionId: transactionId|amount|refundReferenceId|refundType[|refundVpoint nếu có]|timestamp (ưu tiên)
      • Nếu có orderIdreferenceId: orderId|referenceId|amount|refundReferenceId|refundType[|refundVpoint nếu có]|timestamp
      • refundVpoint: Khi gửi refundVpoint (PARTIAL refund từ Payment Session), bắt buộc đưa vào chuỗi ký (trước timestamp) để Payment Hub verify.
      • Amount: amount là số nguyên (integer string), minor unit (VD: 100000 VND). Không chứa ký tự phân cách thập phân.
  • 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 + referenceId HOẶC transactionId
  • Ưu tiên: Nếu truyền cả 3 giá trị (transactionId, orderId, referenceId), hệ thống sẽ ưu tiên sử dụng transactionId và bỏ qua orderIdreferenceId

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 merchant
  • X-Request-ID: Unique request ID (để đảm bảo idempotency)
  • X-Timestamp: Unix timestamp in seconds
  • Content-Type: application/json

Request Body:

Có thể truyền orderIdreferenceId (từ init payment) hoặc transactionId:

{
"orderId": "string",
"referenceId": "string",
"transactionId": "string",
"reason": "string",
"requestedBy": "string",
"secureHash": "string"
}
FieldTypeRequiredMô tả
orderIdstring*ID đơn hàng (bắt buộc nếu không có transactionId)
referenceIdstring*Mã tham chiếu (bắt buộc nếu không có transactionId)
transactionIdstring*ID của giao dịch (bắt buộc nếu không có orderIdreferenceId)
reasonstringLý do hủy giao dịch
requestedBystringNgười yêu cầu hủy
secureHashstringChữ 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ẶC transactionId
  • Ưu tiên: Nếu truyền cả 3 giá trị (transactionId, orderId, referenceId), hệ thống sẽ ưu tiên sử dụng transactionId và bỏ qua orderIdreferenceId

SecureHash Formula (for Cancel/Confirm):

  • Nếu có transactionId: HMAC-SHA-256(secretKey, transactionId + '|' + timestamp) (ưu tiên)
  • Nếu có orderIdreferenceId (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 orderIdreferenceId

Success Response (200):

{
"code": 0,
"message": "Thành công"
}

Error Responses:

HTTP StatusCodeMessageMô tả
4004014Transaction not available for cancelGiao dịch không ở trạng thái có thể hủy
4014100Invalid API keyAPI Key không hợp lệ
4034200Resource does not belong to this userGiao dịch không thuộc merchant/terminal
4044301Transaction not foundKhông tìm thấy giao dịch
5005000Internal server errorLỗ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 merchant
  • X-Request-ID: Unique request ID
  • X-Timestamp: Unix timestamp in seconds
  • Content-Type: application/json

Request Body:

Có thể truyền orderIdreferenceId (từ init payment) hoặc transactionId:

{
"orderId": "string",
"referenceId": "string",
"transactionId": "string",
"secureHash": "string"
}
FieldTypeRequiredMô tả
orderIdstring*ID đơn hàng (bắt buộc nếu không có transactionId)
referenceIdstring*Mã tham chiếu (bắt buộc nếu không có transactionId)
transactionIdstring*ID của giao dịch (bắt buộc nếu không có orderIdreferenceId)
secureHashstringChữ 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ẶC transactionId
  • Ưu tiên: Nếu truyền cả 3 giá trị (transactionId, orderId, referenceId), hệ thống sẽ ưu tiên sử dụng transactionId và bỏ qua orderIdreferenceId

SecureHash Formula (for Cancel/Confirm):

  • Nếu có transactionId: HMAC-SHA-256(secretKey, transactionId + '|' + timestamp) (ưu tiên)
  • Nếu có orderIdreferenceId (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 orderIdreferenceId

Success Response (200):

{
"code": 0,
"message": "Thành công"
}

Error Responses:

HTTP StatusCodeMessageMô tả
4004015Transaction not available for confirmGiao dịch không ở trạng thái có thể confirm
4014100Invalid API keyAPI Key không hợp lệ
4034200Resource does not belong to this userGiao dịch không thuộc merchant/terminal
4044301Transaction not foundKhông tìm thấy giao dịch
5005000Internal server errorLỗ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), VPointPromotion (voucher). Refund áp dụng như sau:

Loại refundPhạm vi hoàn trảCách truyền
FULLHoà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).
PARTIALChỉ hoàn trả phần VPoint và phần tiền (money); không hoàn Promotion/voucher.Truyền refundType: "PARTIAL"bắt buộc truyền đúng số tiền (money) cần hoàn trong amountsố đ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 merchant
  • X-Request-ID: Unique request ID
  • X-User-ID: User ID thực hiện refund
  • X-Timestamp: Unix timestamp in seconds
  • Content-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"
}
FieldTypeRequiredMô tả
amountnumberSố 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.
transactionIdstring*ID của giao dịch cần refund
orderIdstring*ID đơn hàng
referenceIdstring*Mã tham chiếu đơn hàng
currencystringĐơ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.
refundTypestringLoại refund: FULL hoặc PARTIAL
reasonstringLý do hoàn tiền
requestedBystringNgười yêu cầu refund
refundReferenceIdstringReference ID cho refund
refundVpointnumber❌*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).
secureHashstringChữ 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ẶC transactionId
  • Ưu tiên: Nếu truyền cả 3 giá trị (transactionId, orderId, referenceId), hệ thống sẽ ưu tiên sử dụng transactionId và bỏ qua orderIdreferenceId

SecureHash Formula:

  • Nếu có transactionId (ưu tiên):
    • Không có refundVpoint: transactionId|amount|refundReferenceId|refundType|timestamp
    • refundVpoint (PARTIAL từ Payment Session): transactionId|amount|refundReferenceId|refundType|refundVpoint|timestamp
  • Nếu có orderIdreferenceId (và không có transactionId):
    • Không có refundVpoint: orderId|referenceId|amount|refundReferenceId|refundType|timestamp
    • refundVpoint: orderId|referenceId|amount|refundReferenceId|refundType|refundVpoint|timestamp

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
}
}
FieldTypeMô tả
codeintegerMã trạng thái (0 = thành công)
messagestringThông báo kết quả
data.refundIdstringID của refund request
data.remainingRefundableAmountintegerSố tiền còn lại có thể refund (minor unit)

Error Responses:

HTTP StatusCodeMessageMô tả
4004012Transaction not available for refundGiao dịch không ở trạng thái có thể refund
4014100Invalid API keyAPI Key không hợp lệ
4034200Resource does not belong to this userGiao dịch không thuộc merchant/terminal
4044301Transaction not foundKhông tìm thấy giao dịch
5005000Internal server errorLỗ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_idclient_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: ProductionUAT.

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_idclient_secret riê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_idclient_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: ProductionUAT.

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_idclient_secret riê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'