Appearance
Instant Payment Notification (IPN)
Giới thiệu
Instant Payment Notification (IPN) là cơ chế thông báo giao dịch tức thì từ hệ thống Pay2S đến máy chủ của merchant ngay khi có thay đổi trạng thái thanh toán, ví dụ như: giao dịch thành công, thất bại hoặc bị hủy.
Thông báo IPN được gửi tự động dưới dạng HTTP POST đến một địa chỉ URL do merchant cấu hình trong hệ thống Pay2S.
📌 Mục đích của IPN
- Giúp merchant nhận biết tức thời trạng thái giao dịch.
- Cho phép merchant xử lý tự động các hành động sau thanh toán như kích hoạt dịch vụ, gửi hàng, cập nhật trạng thái đơn hàng, v.v.
- Đảm bảo tính tự động, chính xác, và nhanh chóng trong quy trình vận hành.
⚙️ Cơ chế hoạt động
- Merchant tạo đơn hàng thông qua API Pay2S.
- Người dùng tiến hành thanh toán.
- Khi hệ thống Pay2S xác nhận giao dịch, một yêu cầu HTTP POST sẽ được gửi đến IPN URL mà merchant đã cấu hình.
- Thông báo IPN chứa dữ liệu về giao dịch:
transactionId,orderId,amount,status,signature, ... - Merchant cần xác thực chữ ký (
signature) để đảm bảo dữ liệu không bị giả mạo. - Merchant phản hồi HTTP status code
200 OKđể xác nhận đã nhận thành công.
| Attribute | Value | Description |
|---|---|---|
| URL | ipnUrl | URL |
| Method | POST | Phương thức gửi IPN |
| Headers | Content-type: application/json | HTTP Headers |
| Payload | Nội dung JSON giao dịch | Thông tin giao dịch được gửi từ Pay2S |
Mẫu Request IPN
json
curl -X 'POST' 'https://example.com/pay2s_ipn' -H 'content-type: application/json' -d $'{
"partnerCode":"PAY2S",
"orderId":"01234567890123451633504872421",
"requestId":"01234567890123451633504872421",
"amount":1000,
"orderInfo":"Test Thue 1234556",
"orderType":"Pay2S_wallet",
"transId":2588659987,
"resultCode":0,
"message":"Giao dịch thành công.",
"payType":"qr",
"m2signature":"90482b3881bdf863d5f61ace078921bbc6dbb58b2fded35261c71c9af3b1ce4f"
}'Response của đối tác sẽ đến Pay2S sau khi Pay2S gửi HTTP request đến địa chỉ ipnUrl.
IMPORTANT
Đối tác cần phản hồi lại trong vòng 30 giây với:
- HTTP status code:
200 OK - Body:
{ "success": true }(dưới dạng JSON)
Nếu không đúng cấu trúc trên, Pay2S sẽ tự động gửi lại IPN tối đa 5 lần.
INFO
Đối tác cần kiểm tra tính hợp lệ của chữ ký trong IPN để đảm bảo kết quả của giao dịch. Amount và Currency trong notification phải khớp với Amount và Currency đối tác lưu trong database bên đó!
Field resultCode và message do đối tác xử lý. Tham chiếu đến result code mà Pay2S trả về. Pay2S sẽ dùng những thông tin này để phản hồi lại khách hàng nếu có bất cứ lỗi nào xảy ra trong quá trình xử lý thanh toán bên nhà đối tác.
Trạng Thái Giao Dịch - Sử dụng field resultCode để xác định trạng thái của giao dịch:
resultCode = 0: giao dịch thành công.resultCode = 9000: giao dịch được cấp quyền (authorization) thành công .resultCode <> 0: giao dịch thất bại. Tham khảo Result code để xác định chi tiết lỗi của giao dịch.
📘 Một số resultCode phổ biến
| resultCode | Mô tả |
|---|---|
| 0 | Giao dịch thành công |
| 9000 | giao dịch được cấp quyền (authorization) thành công. |
| 2 | Giao dịch đã hủy hoặc không thành công |
| 1002 | Sai chữ ký xác thực |
| 1003 | Giao dịch không tồn tại |
💻 Code mẫu
php
<?php
header("content-type: application/json; charset=UTF-8");
http_response_code(200); //200 - Everything will be 200 Oke
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (is_null($data)) {
echo 'Invalid JSON data.';
exit;
}
function response($data, $code = 200)
{
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data);
die();
}
$accessKey = "";
$secretKey = "";
$response = array();
try {
$amount = $data['amount'];
$extraData = isset($data['extraData']) ? $data['extraData'] : '';
$message = $data['message'];
$orderId = $data['orderId'];
$orderInfo = $data['orderInfo'];
$orderType = $data['orderType'];
$partnerCode = $data['partnerCode'];
$payType = $data['payType'];
$requestId = $data['requestId'];
$responseTime = $data['responseTime'];
$resultCode = $data['resultCode'];
$transId = $data['transId'];
$m2signature = $data['m2signature'];
//Checksum
$rawHash = "accessKey=$accessKey&amount=$amount&extraData=$extraData&message=$message&orderId=$orderId&orderInfo=$orderInfo&orderType=$orderType&partnerCode=$partnerCode&payType=$payType&requestId=$requestId&responseTime=$responseTime&resultCode=$resultCode&transId=$transId";
$partnerSignature = hash_hmac("sha256", $rawHash, $secretKey);
if ($m2signature == $partnerSignature) {
if ($resultCode == '0') {
$result = '<div class="alert alert-success">Capture Payment Success</div>';
} else {
$result = '<div class="alert alert-danger">' . $message . '</div>';
}
} else {
$result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
}
} catch (Exception $e) {
echo $response['message'] = $e;
}
$debugger = array();
$debugger['rawData'] = $rawHash;
$debugger['pay2sSignature'] = $m2signature;
$debugger['partnerSignature'] = $partnerSignature;
if ($m2signature == $partnerSignature) {
$response['success'] = true;
$response['message'] = "Received payment result success";
} else {
$response['success'] = false;
$response['message'] = "ERROR! Fail checksum";
}
$response['debugger'] = $debugger;
echo json_encode($response);js
const express = require("express");
const crypto = require("crypto");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.json());
app.post("/ipn", (req, res) => {
const data = req.body;
// Kiểm tra JSON hợp lệ
if (!data) {
return res
.status(400)
.json({ success: false, message: "Invalid JSON data." });
}
let accessKey = "";
let secretKey = "";
try {
// Lấy dữ liệu từ request body
const amount = data.amount;
const extraData = data.extraData || "";
const message = data.message;
const orderId = data.orderId;
const orderInfo = data.orderInfo;
const orderType = data.orderType;
const partnerCode = data.partnerCode;
const payType = data.payType;
const requestId = data.requestId;
const responseTime = data.responseTime;
const resultCode = data.resultCode;
const transId = data.transId;
const m2signature = data.m2signature;
// Tạo chuỗi `rawHash` cho chữ ký
const rawHash = `accessKey=${accessKey}&amount=${amount}&extraData=${extraData}&message=${message}&orderId=${orderId}&orderInfo=${orderInfo}&orderType=${orderType}&partnerCode=${partnerCode}&payType=${payType}&requestId=${requestId}&responseTime=${responseTime}&resultCode=${resultCode}&transId=${transId}`;
// Tạo chữ ký HMAC SHA256
const partnerSignature = crypto
.createHmac("sha256", secretKey)
.update(rawHash)
.digest("hex");
// Kiểm tra chữ ký
if (m2signature === partnerSignature) {
if (resultCode === "0") {
result =
'<div class="alert alert-success">Capture Payment Success</div>';
} else {
result = `<div class="alert alert-danger">${message}</div>`;
}
} else {
result =
'<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
}
// Debugger thông tin
const debuggerInfo = {
rawData: rawHash,
pay2sSignature: m2signature,
partnerSignature: partnerSignature,
};
// Trả về kết quả
if (m2signature === partnerSignature) {
return res.status(200).json({
success: true,
message: "Received payment result success",
debugger: debuggerInfo,
});
} else {
return res.status(200).json({
success: false,
message: "ERROR! Fail checksum",
debugger: debuggerInfo,
});
}
} catch (error) {
return res.status(500).json({ success: false, message: error.message });
}
});
// Khởi động server
app.listen(3000, () => {
console.log("Server is running on port 3000");
});js
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace Pay2SIPN.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Pay2SController : ControllerBase
{
[HttpPost("ipn")]
public IActionResult IPN([FromBody] Pay2SRequest data)
{
// Kiểm tra JSON hợp lệ
if (data == null)
{
return BadRequest(new { success = false, message = "Invalid JSON data." });
}
string accessKey = ""; // Khóa truy cập của bạn
string secretKey = ""; // Khóa bí mật của bạn
string partnerSignature;
try
{
// Lấy các giá trị từ request body
var amount = data.amount;
var extraData = data.extraData ?? string.Empty;
var message = data.message;
var orderId = data.orderId;
var orderInfo = data.orderInfo;
var orderType = data.orderType;
var partnerCode = data.partnerCode;
var payType = data.payType;
var requestId = data.requestId;
var responseTime = data.responseTime;
var resultCode = data.resultCode;
var transId = data.transId;
var m2signature = data.m2signature;
// Tạo chuỗi rawHash
var rawHash = $"accessKey={accessKey}&amount={amount}&extraData={extraData}&message={message}&orderId={orderId}&orderInfo={orderInfo}&orderType={orderType}&partnerCode={partnerCode}&payType={payType}&requestId={requestId}&responseTime={responseTime}&resultCode={resultCode}&transId={transId}";
// Tạo chữ ký HMAC SHA256
partnerSignature = CreateHmacSha256Signature(rawHash, secretKey);
// Kiểm tra chữ ký
if (m2signature == partnerSignature)
{
if (resultCode == "0")
{
return Ok(new
{
success = true,
message = "Received payment result success",
debugger = new
{
rawData = rawHash,
pay2sSignature = signature,
partnerSignature = partnerSignature
}
});
}
else
{
return Ok(new
{
success = false,
message = message,
debugger = new
{
rawData = rawHash,
pay2sSignature = signature,
partnerSignature = partnerSignature
}
});
}
}
else
{
return Ok(new
{
success = false,
message = "This transaction could be hacked, please check your signature and returned signature",
debugger = new
{
rawData = rawHash,
pay2sSignature = signature,
partnerSignature = partnerSignature
}
});
}
}
catch (Exception ex)
{
return StatusCode(500, new { success = false, message = ex.Message });
}
}
// 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();
}
}
}
// Lớp đại diện cho dữ liệu yêu cầu từ Pay2S
public class Pay2SRequest
{
public string amount { get; set; }
public string extraData { get; set; }
public string message { get; set; }
public string orderId { get; set; }
public string orderInfo { get; set; }
public string orderType { get; set; }
public string partnerCode { get; set; }
public string payType { get; set; }
public string requestId { get; set; }
public string responseTime { get; set; }
public string resultCode { get; set; }
public string transId { get; set; }
public string m2signature { get; set; }
}
}python
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
def verify_signature(data, signature, secret_key):
"""Xác thực chữ ký HMAC SHA256"""
raw_hash = f"accessKey={data.get('accessKey')}&amount={data.get('amount')}&extraData={data.get('extraData', '')}&message={data.get('message')}&orderId={data.get('orderId')}&orderInfo={data.get('orderInfo')}&orderType={data.get('orderType')}&partnerCode={data.get('partnerCode')}&payType={data.get('payType')}&requestId={data.get('requestId')}&responseTime={data.get('responseTime')}&resultCode={data.get('resultCode')}&transId={data.get('transId')}"
computed_signature = hmac.new(
secret_key.encode('utf-8'),
raw_hash.encode('utf-8'),
hashlib.sha256
).hexdigest()
return computed_signature == signature
@app.route('/ipn', methods=['POST'])
def handle_ipn():
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'message': 'Invalid JSON data'}), 400
access_key = "" # Khóa truy cập
secret_key = "" # Khóa bí mật
m2signature = data.get('m2signature')
result_code = data.get('resultCode')
message = data.get('message')
# Xác thực chữ ký
if verify_signature(data, m2signature, secret_key):
if result_code == '0':
return jsonify({
'success': True,
'message': 'Received payment result success'
}), 200
else:
return jsonify({
'success': False,
'message': message
}), 200
else:
return jsonify({
'success': False,
'message': 'This transaction could be hacked, please check your signature'
}), 200
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
if __name__ == '__main__':
app.run(port=3000, debug=True)java
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/api")
public class Pay2SIPNController {
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();
}
@PostMapping("/ipn")
public ResponseEntity<?> handleIPN(@RequestBody JsonObject data) {
try {
if (data == null) {
return ResponseEntity.badRequest()
.body(new Gson().toJson(new IResponseData(false, "Invalid JSON data")));
}
String accessKey = ""; // Khóa truy cập
String secretKey = ""; // Khóa bí mật
// Lấy dữ liệu từ request
String amount = data.get("amount").getAsString();
String extraData = data.has("extraData") ? data.get("extraData").getAsString() : "";
String message = data.get("message").getAsString();
String orderId = data.get("orderId").getAsString();
String orderInfo = data.get("orderInfo").getAsString();
String orderType = data.get("orderType").getAsString();
String partnerCode = data.get("partnerCode").getAsString();
String payType = data.get("payType").getAsString();
String requestId = data.get("requestId").getAsString();
String responseTime = data.get("responseTime").getAsString();
String resultCode = data.get("resultCode").getAsString();
String transId = data.get("transId").getAsString();
String m2signature = data.get("m2signature").getAsString();
// Tạo chuỗi rawHash
String rawHash = String.format(
"accessKey=%s&amount=%s&extraData=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s&transId=%s",
accessKey, amount, extraData, message, orderId, orderInfo, orderType,
partnerCode, payType, requestId, responseTime, resultCode, transId
);
// Tạo chữ ký
String partnerSignature = createSignature(rawHash, secretKey);
// Kiểm tra chữ ký
if (m2signature.equals(partnerSignature)) {
if ("0".equals(resultCode)) {
return ResponseEntity.ok(
new Gson().toJson(new IResponseData(true, "Received payment result success"))
);
} else {
return ResponseEntity.ok(
new Gson().toJson(new IResponseData(false, message))
);
}
} else {
return ResponseEntity.ok(
new Gson().toJson(new IResponseData(false, "This transaction could be hacked"))
);
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new Gson().toJson(new IResponseData(false, e.getMessage())));
}
}
static class IResponseData {
boolean success;
String message;
IResponseData(boolean success, String message) {
this.success = success;
this.message = message;
}
}
}go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
type IPNData struct {
Amount string `json:"amount"`
ExtraData string `json:"extraData"`
Message string `json:"message"`
OrderID string `json:"orderId"`
OrderInfo string `json:"orderInfo"`
OrderType string `json:"orderType"`
PartnerCode string `json:"partnerCode"`
PayType string `json:"payType"`
RequestID string `json:"requestId"`
ResponseTime string `json:"responseTime"`
ResultCode string `json:"resultCode"`
TransID string `json:"transId"`
M2Signature string `json:"m2signature"`
}
type ResponseData struct {
Success bool `json:"success"`
Message string `json:"message"`
}
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 handleIPN(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading body", http.StatusBadRequest)
return
}
defer r.Body.Close()
var data IPNData
err = json.Unmarshal(body, &data)
if err != nil {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ResponseData{Success: false, Message: "Invalid JSON data"})
return
}
accessKey := "" // Khóa truy cập
secretKey := "" // Khóa bí mật
// Tạo chuỗi rawHash
rawHash := fmt.Sprintf(
"accessKey=%s&amount=%s&extraData=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s&transId=%s",
accessKey, data.Amount, data.ExtraData, data.Message, data.OrderID, data.OrderInfo,
data.OrderType, data.PartnerCode, data.PayType, data.RequestID, data.ResponseTime,
data.ResultCode, data.TransID,
)
// Tạo chữ ký
partnerSignature := createSignature(rawHash, secretKey)
w.Header().Set("Content-Type", "application/json")
// Kiểm tra chữ ký
if data.M2Signature == partnerSignature {
if data.ResultCode == "0" {
json.NewEncoder(w).Encode(ResponseData{Success: true, Message: "Received payment result success"})
} else {
json.NewEncoder(w).Encode(ResponseData{Success: false, Message: data.Message})
}
} else {
json.NewEncoder(w).Encode(ResponseData{Success: false, Message: "This transaction could be hacked"})
}
}
func main() {
http.HandleFunc("/ipn", handleIPN)
log.Println("Server running on port 3000")
log.Fatal(http.ListenAndServe(":3000", nil))
}ruby
require 'sinatra'
require 'json'
require 'openssl'
def verify_signature(data, signature, secret_key)
raw_hash = "accessKey=#{data['accessKey']}&amount=#{data['amount']}&extraData=#{data['extraData'] || ''}&message=#{data['message']}&orderId=#{data['orderId']}&orderInfo=#{data['orderInfo']}&orderType=#{data['orderType']}&partnerCode=#{data['partnerCode']}&payType=#{data['payType']}&requestId=#{data['requestId']}&responseTime=#{data['responseTime']}&resultCode=#{data['resultCode']}&transId=#{data['transId']}"
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_hash)
computed_signature == signature
end
post '/ipn' do
content_type :json
begin
data = JSON.parse(request.body.read)
if data.nil?
return { success: false, message: 'Invalid JSON data' }.to_json
end
access_key = '' # Khóa truy cập
secret_key = '' # Khóa bí mật
signature = data['m2signature']
result_code = data['resultCode']
message = data['message']
# Xác thực chữ ký
if verify_signature(data, signature, secret_key)
if result_code == '0'
{ success: true, message: 'Received payment result success' }.to_json
else
{ success: false, message: message }.to_json
end
else
{ success: false, message: 'This transaction could be hacked, please check your signature' }.to_json
end
rescue => e
{ success: false, message: e.message }.to_json
end
end🔧 Instant Payment Notification (IPN)
Instant Payment Notification (IPN) là thông điệp được gửi từ Nhà Cung Cấp Dịch Vụ Thanh Toán - Payment Service Provider (PSP) đến Bên Sử Dụng Dịch Vụ Thanh Toán - Payment Service Consumer (PSC). Việc này sử dụng giao thức HTTP và quy trình này là bất đồng bộ.
Tại sao sử dụng IPN
Instant Payment Notification (IPN) được sử dụng để thông báo (notify) kết quả giao dịch ngay lập tức đến PSC:
Thanh Toán: Web, SmartTv.
Thanh Toán Trực Tuyến
Thanh Toán Định Kỳ Đối tác có thể sử dụng hệ thống của mình để xử lý thông tin nhận được từ PSP:
Cập nhật trạng thái giao dịch (khuyên dùng)
Cập nhật số dư tài khoản (nạp tiền), xuất sản phẩm, thông báo kết quả giao dịch đến ứng dụng di động,...
Gửi hóa đơn điện tử
Sử dụng IPN để khắc phục trường hợp thanh toán thành công nhưng người dùng không nhận được sản phẩm.
Lý do cần IPN:
- Người dùng đóng trình duyệt
- Không thể điều hướng đến trang ban đầu của đối tác: do đường truyền, hệ thống quá tải,...
Cách thực thi IPN
Đối tác cần tạo API phía backend để 'lắng nghe' kết quả từ Pay2S và cung cấp URL của API đó trong field ipnUrl.
Sau khi người dùng sử dụng thanh toán trên Pay2S (web hoặc app), Pay2S sẽ thông báo kết quả giao dịch ngay lập tức đến URL này. API của đối tác nhận kết quả thanh toán và tiếp tục xử lý trong hệ thống bên họ.
📋 Mô tả chi tiết các IPN Parameters
| Tham số | Loại | Mô tả |
|---|---|---|
| partnerCode | String | Mã đối tác |
| orderId | String | Mã đơn hàng |
| requestId | String | Mã yêu cầu |
| amount | Long | Số tiền giao dịch (đơn vị: VND) |
| orderInfo | String | Thông tin đơn hàng |
| orderType | String | Loại đơn hàng |
| transId | Long | ID giao dịch tại Pay2S |
| resultCode | Int | 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 | Thời gian phản hồi (format: yyyyMMddHHmmss) |
| extraData | String | Dữ liệu bổ sung từ merchant |
| m2signature | String | Chữ ký HMAC SHA256 xác thực |
⚠️ Lưu ý quan trọng
CẢNH BÁO
- Xác thực chữ ký bắt buộc: Đây là bước không thể bỏ qua để đảm bảo yêu cầu từ Pay2S chứ không phải kẻ xấu.
- Kiểm tra amount: Đảm bảo số tiền nhận được khớp với số tiền lưu trong database.
- Kiểm tra orderId: Giao dịch phải tồn tại trong hệ thống của bạn.
- Phản hồi trong 30 giây: Tốc độ xử lý rất quan trọng, nếu quá chậm Pay2S sẽ retry.
- Retry logic: Pay2S sẽ gửi lại tối đa 5 lần, nên cần xử lý idempotent (gọi nhiều lần được kết quả như nhau).
- Log tất cả: Lưu lại raw data, signature, và kết quả xác thực để debug.
📊 Quy trình xử lý IPN
1. Người dùng thanh toán thành công trên Pay2S
↓
2. Pay2S gửi HTTP POST đến ipnUrl
↓
3. Backend nhận JSON request
↓
4. Xác thực signature (HMAC SHA256)
↓
5. Nếu signature sai → Log lỗi → Trả về 200 OK
↓
6. Kiểm tra orderId tồn tại?
├─ Không → Trả về 200 OK
└─ Có → Tiếp tục
↓
7. Kiểm tra amount khớp?
├─ Không → Log cảnh báo → Trả về 200 OK
└─ Có → Tiếp tục
↓
8. Kiểm tra resultCode
├─ 0 (Thành công) → Cập nhật DB → Gửi sản phẩm/dịch vụ
└─ ≠ 0 (Thất bại) → Cập nhật trạng thái thất bại
↓
9. Trả về { "success": true } với HTTP 200 OK🔄 Retry Logic của Pay2S
Nếu nhận được status code khác 200 hoặc không có response trong 30 giây, Pay2S sẽ gửi lại:
- Lần 1: Ngay sau thanh toán
- Lần 2: 5 phút sau
- Lần 3: 15 phút sau
- Lần 4: 1 giờ sau
- Lần 5: 24 giờ sau
Mẹo: Lưu (orderId, transId) vào cache/DB với TTL 24 giờ để đếm số lần IPN nhận được.
❓ 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 từ Pay2S, ngăn chặn fake request từ kẻ xấu.
Q: Có thể nhận IPN nhiều lần không? A: Có, do network error hoặc timeout. Cần kiểm tra (orderId, transId) trước khi xử lý lần thứ 2.
Q: Phải trả về gì? A: HTTP 200 OK + Body: {"success": true} (JSON) trong vòng 30 giây.
Q: Nếu backend bị lỗi sao? A: Trả về HTTP 500 hoặc không trả về trong 30s, Pay2S sẽ retry.
Q: resultCode là gì? A: Mã trạng thái giao dịch:
0= Thành công9000= Đã được xác thực (authorization)- Khác = Thất bại (xem bảng resultCode phổ biến trên)
Q: Có cách test IPN không? A: Sử dụng Postman/curl gửi POST request tới endpoint của bạn với sample JSON ở trên.
🔐 Best Practices
- Luôn HTTPS: Endpoint phải là HTTPS, không dùng HTTP
- Validate input: Kiểm tra type, format của tất cả tham số
- Idempotent: Xử lý lại IPN cũ không gây hậu quả
- Async processing: Offload việc xử lý nặng vào queue (Redis, RabbitMQ)
- Monitoring: Alert khi có lỗi xác thực signature hoặc amount mismatch
- Database transaction: Dùng transaction để cập nhật trạng thái đơn hàng
- Timeout handling: Set timeout 20-25s để kịp trả về trong 30s
