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>"
}
```