Xử lý kết quả thanh toán (Payment Notification)
Nội dung Markdown đầy đủ của trang tài liệu.
# Xử lý kết quả thanh toán (Payment Notification)
> Đối chiếu luồng hiện tại ngày 11/10/2026: callback trình duyệt là **GET redirectUrl** với query đã ký. `responseTime` tính bằng **giây**, không phải mili giây như IPN. `requestId` là ID nội bộ. `extraData` và `transId` không nằm trong chuỗi ký redirect hiện tại. Chỉ dùng trang này hiển thị kết quả; cập nhật thanh toán bền vững qua IPN hoặc kiểm tra server.
## 🔁 Giao diện Redirect (Client)
Sau khi luồng thanh toán hoàn tất, khách hàng được điều hướng đến `redirectUrl` mà bên đối tác đã cung cấp trong `create` request. Một vài thông số sẽ được thêm vào URL dưới dạng query parameters:
- **Method**: GET
- **Cấu trúc**: `redirectUrl?partnerCode=xxx&orderId=xxx&...`
### 📋 Query Parameters
| Parameter | Loại | Mô tả |
|-----------|------|-------|
| partnerCode | String | Mã đối tác |
| orderId | String | Mã đơn hàng |
| requestId | String | Mã yêu cầu |
| amount | String | Số tiền |
| orderInfo | String | Thông tin đơn hàng |
| orderType | String | Loại đơn hàng |
| transId | String | ID giao dịch (nếu có) |
| resultCode | String | Mã kết quả (0 = thành công) |
| message | String | Thông báo kết quả |
| payType | String | Phương thức thanh toán |
| responseTime | String | Unix timestamp giây trong query |
| m2signature | String | Chữ ký xác thực |
## ⚠️ Lưu ý quan trọng
:::: warning CẢNH BÁO
1. **Luôn xác thực chữ ký**: Đây là bước bắt buộc để đảm bảo yêu cầu đến từ Pay2S chứ không phải từ kẻ xấu.
2. **Kiểm tra Amount**: Đảm bảo số tiền nhận được khớp với số tiền lưu trong database của bạn.
3. **Xử lý idempotent**: Có thể nhận được redirect nhiều lần cho cùng một giao dịch - cần kiểm tra `orderId` trước khi xử lý.
4. **Timeout**: Nên xử lý nhanh chóng, không nên để người dùng chờ lâu.
5. **Log tất cả**: Lưu lại các tham số nhận được để debug sau này.
::::
## 💻 Code mẫu
::: code-group
```php [PHP]
<?php
header('Content-type: text/html; charset=utf-8');
$accessKey = '';
$secretKey = '';
if (!empty($_GET)) {
$partnerCode = $_GET["partnerCode"];
$orderId = $_GET["orderId"];
$requestId = $_GET["requestId"];
$amount = $_GET["amount"];
$orderInfo = $_GET["orderInfo"];
$orderType = $_GET["orderType"];
$transId = $_GET["transId"] ?? '';
$resultCode = $_GET["resultCode"];
$message = $_GET["message"];
$payType = $_GET["payType"];
$responseTime = $_GET["responseTime"];
$extraData = $_GET["extraData"] ?? '';
$m2signature = $_GET["m2signature"]; // Pay2S signature
// Tạo chuỗi hash để xác thực chữ ký
$rawHash = "accessKey=$accessKey&amount=$amount&message=$message&orderId=$orderId&orderInfo=$orderInfo&orderType=$orderType&partnerCode=$partnerCode&payType=$payType&requestId=$requestId&responseTime=$responseTime&resultCode=$resultCode";
$partnerSignature = hash_hmac("sha256", $rawHash, $secretKey);
// Kiểm tra chữ ký
if ($m2signature == $partnerSignature) {
if ($resultCode == '0') {
$result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>';
} else {
$result = '<div class="alert alert-danger"><strong>Payment status: </strong>' . $message . '</div>';
}
} else {
$result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
}
}
```
```js [NodeJS]
const express = require('express');
const crypto = require('crypto');
const app = express();
const port = 3000;
app.get('/redirect', (req, res) => {
const accessKey = ''; // Thay bằng accessKey thực tế
const secretKey = ''; // Thay bằng secretKey thực tế
// Lấy các giá trị từ query parameters
const partnerCode = req.query.partnerCode;
const orderId = req.query.orderId;
const requestId = req.query.requestId;
const amount = req.query.amount;
const orderInfo = req.query.orderInfo;
const orderType = req.query.orderType;
const transId = req.query.transId || '';
const resultCode = req.query.resultCode;
const message = req.query.message;
const payType = req.query.payType;
const responseTime = req.query.responseTime;
const extraData = req.query.extraData || '';
const m2signature = req.query.m2signature; // Pay2S signature
// Tạo chuỗi rawHash
const rawHash = `accessKey=${accessKey}&amount=${amount}&message=${message}&orderId=${orderId}&orderInfo=${orderInfo}&orderType=${orderType}&partnerCode=${partnerCode}&payType=${payType}&requestId=${requestId}&responseTime=${responseTime}&resultCode=${resultCode}`;
// Tạo chữ ký HMAC SHA256
const partnerSignature = crypto.createHmac('sha256', secretKey).update(rawHash).digest('hex');
console.log('Debug rawHash:', rawHash);
console.log('Debug partnerSignature:', partnerSignature);
let result;
// Kiểm tra chữ ký
if (m2signature === partnerSignature) {
if (resultCode === '0') {
result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>';
} else {
result = `<div class="alert alert-danger"><strong>Payment status: </strong>${message}</div>`;
}
} else {
result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
}
// Trả về kết quả
res.send(result);
});
// Khởi động server
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
```
```js [ASP]
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
namespace Pay2SRedirect.Controllers
{
public class PaymentController : Controller
{
// Hành động xử lý redirect
[HttpGet("redirect")]
public IActionResult RedirectFromPay2S(
string partnerCode, string orderId, string requestId,
string amount, string orderInfo, string orderType,
string transId, string resultCode, string message,
string payType, string responseTime, string extraData,
string m2signature)
{
string accessKey = ""; // Thay thế với accessKey thực tế
string secretKey = ""; // Thay thế với secretKey thực tế
// Tạo chuỗi rawHash cho chữ ký
var rawHash = $"accessKey={accessKey}&amount={amount}&message={message}&orderId={orderId}&orderInfo={orderInfo}&orderType={orderType}&partnerCode={partnerCode}&payType={payType}&requestId={requestId}&responseTime={responseTime}&resultCode={resultCode}";
// Tạo chữ ký HMAC SHA256
var partnerSignature = CreateHmacSha256Signature(rawHash, secretKey);
// Kiểm tra chữ ký
if (m2signature == partnerSignature)
{
if (resultCode == "0")
{
// Trả về kết quả thành công
ViewBag.Result = "<div class='alert alert-success'><strong>Payment status: </strong>Success</div>";
}
else
{
// Trả về kết quả lỗi
ViewBag.Result = $"<div class='alert alert-danger'><strong>Payment status: </strong>{message}</div>";
}
}
else
{
// Thông báo chữ ký không hợp lệ
ViewBag.Result = "<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>";
}
// Trả về view chứa kết quả
return View();
}
// Hàm tạo chữ ký HMAC SHA256
private string CreateHmacSha256Signature(string data, string secretKey)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
}
}
```
```python [Python]
from flask import Flask, request, render_template_string
import hmac
import hashlib
app = Flask(__name__)
def verify_signature(params, signature, secret_key):
"""Xác thực chữ ký HMAC SHA256"""
raw_hash = f"accessKey={params.get('accessKey')}&amount={params.get('amount')}&message={params.get('message')}&orderId={params.get('orderId')}&orderInfo={params.get('orderInfo')}&orderType={params.get('orderType')}&partnerCode={params.get('partnerCode')}&payType={params.get('payType')}&requestId={params.get('requestId')}&responseTime={params.get('responseTime')}&resultCode={params.get('resultCode')}"
computed_signature = hmac.new(
secret_key.encode('utf-8'),
raw_hash.encode('utf-8'),
hashlib.sha256
).hexdigest()
return computed_signature == signature
@app.route('/redirect')
def redirect_from_pay2s():
access_key = "" # Khóa truy cập
secret_key = "" # Khóa bí mật
# Lấy các giá trị từ query parameters
params = request.args.to_dict()
partner_code = params.get('partnerCode')
order_id = params.get('orderId')
request_id = params.get('requestId')
amount = params.get('amount')
order_info = params.get('orderInfo')
order_type = params.get('orderType')
trans_id = params.get('transId', '')
result_code = params.get('resultCode')
message = params.get('message')
pay_type = params.get('payType')
response_time = params.get('responseTime')
extra_data = params.get('extraData', '')
m2signature = params.get('m2signature') # Pay2S signature
# Xác thực chữ ký
if verify_signature(params, m2signature, secret_key):
if result_code == '0':
result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>'
else:
result = f'<div class="alert alert-danger"><strong>Payment status: </strong>{message}</div>'
else:
result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>'
return render_template_string('''
<!DOCTYPE html>
<html>
<head>
<title>Payment Result</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
{{ result | safe }}
</div>
</body>
</html>
''', result=result)
if __name__ == '__main__':
app.run(port=3000, debug=True)
```
```java [Java]
import com.google.gson.Gson;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
@Controller
public class Pay2SRedirectController {
private static String createSignature(String rawData, String secretKey) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
0,
secretKey.getBytes(StandardCharsets.UTF_8).length,
"HmacSHA256"
);
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(rawData.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
@GetMapping("/redirect")
public String redirectFromPay2S(
@RequestParam String partnerCode,
@RequestParam String orderId,
@RequestParam String requestId,
@RequestParam String amount,
@RequestParam String orderInfo,
@RequestParam String orderType,
@RequestParam(required = false) String transId,
@RequestParam String resultCode,
@RequestParam String message,
@RequestParam String payType,
@RequestParam String responseTime,
@RequestParam(required = false) String extraData,
@RequestParam String m2signature,
Model model) {
String accessKey = ""; // Khóa truy cập
String secretKey = ""; // Khóa bí mật
try {
// Tạo chuỗi rawHash
String rawHash = String.format(
"accessKey=%s&amount=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s",
accessKey, amount, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode
);
// Tạo chữ ký
String partnerSignature = createSignature(rawHash, secretKey);
String result;
// Kiểm tra chữ ký
if (m2signature.equals(partnerSignature)) {
if ("0".equals(resultCode)) {
result = "<div class='alert alert-success'><strong>Payment status: </strong>Success</div>";
} else {
result = "<div class='alert alert-danger'><strong>Payment status: </strong>" + message + "</div>";
}
} else {
result = "<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>";
}
model.addAttribute("result", result);
} catch (Exception e) {
model.addAttribute("result", "<div class='alert alert-danger'>Error: " + e.getMessage() + "</div>");
}
return "payment-result";
}
}
```
```go [Go]
package main
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
"html/template"
"log"
"net/http"
)
func createSignature(rawData, secretKey string) string {
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(rawData))
return fmt.Sprintf("%x", h.Sum(nil))
}
func redirectHandler(w http.ResponseWriter, r *http.Request) {
accessKey := "" // Khóa truy cập
secretKey := "" // Khóa bí mật
// Parse query parameters
err := r.ParseForm()
if err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
return
}
partnerCode := r.FormValue("partnerCode")
orderId := r.FormValue("orderId")
requestId := r.FormValue("requestId")
amount := r.FormValue("amount")
orderInfo := r.FormValue("orderInfo")
orderType := r.FormValue("orderType")
resultCode := r.FormValue("resultCode")
message := r.FormValue("message")
payType := r.FormValue("payType")
responseTime := r.FormValue("responseTime")
m2signature := r.FormValue("m2signature")
// Tạo chuỗi rawHash
rawHash := fmt.Sprintf(
"accessKey=%s&amount=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s",
accessKey, amount, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode,
)
// Tạo chữ ký
partnerSignature := createSignature(rawHash, secretKey)
var result template.HTML
// Kiểm tra chữ ký
if m2signature == partnerSignature {
if resultCode == "0" {
result = template.HTML("<div class='alert alert-success'><strong>Payment status: </strong>Success</div>")
} else {
result = template.HTML(fmt.Sprintf("<div class='alert alert-danger'><strong>Payment status: </strong>%s</div>", message))
}
} else {
result = template.HTML("<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>")
}
// Trả về HTML
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl := template.Must(template.New("result").Parse(`
<!DOCTYPE html>
<html>
<head>
<title>Payment Result</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
{{ . }}
</div>
</body>
</html>
`))
tmpl.Execute(w, result)
}
func main() {
http.HandleFunc("/redirect", redirectHandler)
log.Println("Server running on port 3000")
log.Fatal(http.ListenAndServe(":3000", nil))
}
```
```ruby [Ruby]
require 'sinatra'
require 'openssl'
require 'erb'
def verify_signature(params, signature, secret_key)
raw_hash = "accessKey=#{params['accessKey']}&amount=#{params['amount']}&message=#{params['message']}&orderId=#{params['orderId']}&orderInfo=#{params['orderInfo']}&orderType=#{params['orderType']}&partnerCode=#{params['partnerCode']}&payType=#{params['payType']}&requestId=#{params['requestId']}&responseTime=#{params['responseTime']}&resultCode=#{params['resultCode']}"
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_hash)
computed_signature == signature
end
get '/redirect' do
access_key = '' # Khóa truy cập
secret_key = '' # Khóa bí mật
partner_code = params['partnerCode']
order_id = params['orderId']
request_id = params['requestId']
amount = params['amount']
order_info = params['orderInfo']
order_type = params['orderType']
trans_id = params['transId'] || ''
result_code = params['resultCode']
message = params['message']
pay_type = params['payType']
response_time = params['responseTime']
extra_data = params['extraData'] || ''
m2signature = params['m2signature']
# Xác thực chữ ký
if verify_signature(params, m2signature, secret_key)
if result_code == '0'
result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>'
else
result = "<div class=\"alert alert-danger\"><strong>Payment status: </strong>#{message}</div>"
end
else
result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>'
end
erb :payment_result, locals: { result: result }
end
```
:::
## 📋 Quy trình xử lý Redirect
```
1. Người dùng thanh toán thành công trên Pay2S
↓
2. Pay2S chuyển hướng người dùng đến redirectUrl với query parameters
↓
3. Backend nhận các tham số từ URL
↓
4. Xác thực chữ ký m2signature
↓
5. Kiểm tra resultCode:
- 0: Giao dịch thành công → Cập nhật database
- ≠ 0: Giao dịch thất bại → Hiển thị thông báo lỗi
↓
6. Hiển thị kết quả cho người dùng
```
## 🔍 Kết hợp với IPN
- **Redirect**: Thông báo ngay cho người dùng thấy kết quả trên browser
- **IPN**: Thông báo cho backend để xử lý tự động và lưu database
Khuyến cáo: Sử dụng cả hai để đảm bảo không bị mất thông tin giao dịch.
## ❓ Câu hỏi thường gặp
**Q: Tại sao cần xác thực chữ ký?**
A: Để đảm bảo yêu cầu đích thực đến từ Pay2S, không phải từ kẻ xấu giả mạo.
**Q: Điều gì xảy ra nếu người dùng không nhận được redirect?**
A: Pay2S sẽ gửi IPN để backend xử lý, sau đó người dùng có thể kiểm tra trạng thái thủ công.
**Q: Có thể nhận được redirect nhiều lần không?**
A: Có, do mạng hoặc lỗi khác. Vì thế cần kiểm tra `orderId` trước khi xử lý.
**Q: resultCode là gì?**
A: Mã trạng thái giao dịch:
- `0` = Thành công
- `9000` = Đã được xác thực (authorization)
- `>0` = Thất bại (xem IPN doc để biết mã lỗi cụ thể)
## Request và response đối chiếu ngày 11/10/2026
Các mẫu dưới đây giữ cấu trúc trường và kiểu dữ liệu. Khóa, chữ ký, URL phiên đã được thay bằng placeholder; danh sách chỉ giữ tối đa hai phần tử và dữ liệu ảnh/PDF/XML dài được rút gọn. Không sao chép placeholder để gọi API thật.
### Payment Notification · URL quay lại
<div class="doc-api-actions" data-doc-api-actions><a class="try-button" data-doc-playground href="/playground/?api=return">Thử API trong Playground ↗</a><a class="try-button experience-button" data-doc-experience href="/demos/?flow=payment&api=return">Trải nghiệm thanh toán & IPN ↗</a></div>
**Đã gọi API demo/UAT.** Đây là API dùng bởi trang payment để lấy redirectUrl. Callback tới website khách hàng là GET redirectUrl với query đã ký; không phải POST API tạo đơn.
`POST /api/generate-redirect`
Body:
```json
{
"t": "<T>"
}
```
HTTP 200. Response:
```json
{
"success": true,
"paid": true,
"redirectUrl": "https://developer.example.com/demos/?mode=live&accessKey=[REDACTED]&amount=250000&message=Payment+successful&orderId=DEVd33eb13817916599723201625c7&orderInfo=TT817916599723201625c7&orderType=pay2s&partnerCode=PAY2S7EPF0SB1ZP27W71&payType=pay2s&requestId=1583231&responseTime=1791659973&resultCode=0&m2signature=<REDACTED>"
}
```