# Instant Payment Notification (IPN)
Pay2S gửi POST JSON từ server tới ipnUrl của đơn hàng khi đơn hoàn tất hoặc bị hủy. IPN không phụ thuộc khách còn mở trang thanh toán. Đây là luồng khác với webhook biến động số dư và URL quay lại trên trình duyệt.
Payload hiện tại
Payload đầy đủ đối chiếu worker ngày 11/10/2026 nằm ở cuối trang.
| Trường | Ý nghĩa |
|---|---|
accessKey, partnerCode |
Khóa công khai và mã đối tác của bộ khóa tạo đơn |
orderId |
Mã đơn merchant truyền khi tạo thanh toán |
requestId |
ID hóa đơn nội bộ Pay2S, không phải requestId gốc của merchant |
requestTrace |
requestId merchant đã gửi lúc tạo đơn |
amount |
Số tiền đã trả khi thành công; số tiền đơn khi hủy |
orderInfo |
Nội dung đơn |
orderType, payType |
Loại thanh toán của đơn |
extraData |
Worker hiện gửi chuỗi rỗng; không trả nguyên metadata V2 |
message |
Paymentsuccessful hoặc Paymentcancelled |
resultCode |
0: đã thanh toán; 2: hủy đơn |
transId |
Mã hóa đơn Pay2S, dạng chuỗi |
responseTime |
Unix timestamp mili giây; URL quay lại dùng giây |
m2signature |
HMAC-SHA256 dạng hex |
requestId, amount có thể là số hoặc chuỗi số tùy backend/driver. Giữ nguyên giá trị khi tạo chuỗi ký; không thêm dấu phân cách hàng nghìn.
Xác minh chữ ký
Dùng Secret Key của đúng bộ khóa đã tạo đơn (tài khoản chính hoặc cửa hàng). Ghép đúng thứ tự, không URL-encode và không thêm xuống dòng:
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}
m2signature = HMAC-SHA256(secretKey, chuỗi_trên). Không đưa requestTrace hoặc m2signature vào chuỗi ký. Chữ ký IPN khác chữ ký tạo đơn V1/V2 và khác chữ ký URL quay lại.
Xử lý và ACK
- Xác minh chữ ký, Partner Code và Access Key.
- Tìm đơn theo
orderId, đối chiếu số tiền, chống xử lý trùng trong transaction của database. resultCode=0: cập nhật thanh toán đúng một lần.resultCode=2: xử lý hủy; không ghi nhận doanh thu hoặc hạ trạng thái đơn đã xác nhận thanh toán.- Lưu kết quả bền vững rồi trả HTTP 200 và JSON bên dưới. Nếu cùng kết quả đã được xử lý thì vẫn ACK thành công.
{ "success": true }
Worker dùng timeout 10 giây. Khi thất bại, lần retry đầu lên lịch sau 5 giây, tiếp theo 15, 60, 300 giây; dừng khi bộ đếm lỗi đạt 5. Thời điểm thực nhận phụ thuộc worker và mạng. Không trả HTML hoặc chỉ 200 với body rỗng. ACK cũ chứa thông điệp Received payment result success vẫn được chấp nhận; tích hợp mới dùng success: true.
Xác minh bằng PHP
<?php
function verifyPay2SIpn(array $body, string $secret): bool {
$fields = ['accessKey','amount','extraData','message','orderId','orderInfo',
'orderType','partnerCode','payType','requestId','responseTime','resultCode','transId'];
if ($secret === '') return false;
$parts = [];
foreach ($fields as $key) {
if (!array_key_exists($key, $body) || !is_scalar($body[$key])) return false;
$parts[] = $key . '=' . $body[$key];
}
$signature = $body['m2signature'] ?? null;
return is_string($signature) && hash_equals(
hash_hmac('sha256', implode('&', $parts), $secret), $signature
);
}
// Gọi với JSON body và Secret Key lấy từ biến môi trường backend.
// Sau xác minh: đối chiếu Partner Code/Access Key/đơn/số tiền, lưu DB chống trùng,
// rồi trả HTTP 200, Content-Type application/json, body {"success":true}.
Xác minh bằng Node.js
import crypto from 'node:crypto';
export function verifyPay2SIpn(body, secretKey) {
const fields = ['accessKey','amount','extraData','message','orderId','orderInfo',
'orderType','partnerCode','payType','requestId','responseTime','resultCode','transId'];
if (!secretKey || !body || fields.some(k => !['string','number'].includes(typeof body[k]))) return false;
if (!/^[a-f0-9]{64}$/.test(body.m2signature || '')) return false;
const raw = fields.map(k => `${k}=${body[k]}`).join('&');
const expected = crypto.createHmac('sha256', secretKey).update(raw).digest();
return crypto.timingSafeEqual(expected, Buffer.from(body.m2signature, 'hex'));
}
// Sau xác minh: đối chiếu Partner Code/Access Key/đơn/số tiền, lưu DB chống trùng,
// rồi res.status(200).json({success:true}).
Các hàm trên chỉ xác minh chữ ký. Cần nối nghiệp vụ và database của ứng dụng trước khi ACK; không log Secret Key hoặc nguyên thông tin xác thực.
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.
IPN · Gửi, nhận & xác minh chữ ký
Đối chiếu mã nguồn backend; giá trị minh họa. Payload từ sendIPN.js. Hủy đơn: resultCode=2, message=Paymentcancelled. requestTrace là requestId lúc tạo đơn; requestId là ID hóa đơn nội bộ.
POST {ipnUrl}
Body:
{
"accessKey": "YOUR_ACCESS_KEY",
"amount": 250000,
"extraData": "",
"message": "Paymentsuccessful",
"orderId": "ORDER_DEMO_001",
"orderInfo": "THANHTOANDEMO",
"orderType": "pay2s",
"partnerCode": "YOUR_PARTNER_CODE",
"payType": "pay2s",
"requestId": 10001,
"requestTrace": "REQUEST_DEMO_001",
"responseTime": 1791691200000,
"resultCode": 0,
"transId": "PAY2S_DEMO_INVOICE",
"m2signature": "<HMAC_SHA256_HEX>"
}
HTTP 200. Response:
{
"success": true
}