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

Kiểm tra dữ liệu với signature

Kiểm tra thông tin bằng chữ ký (payment-requests)

Giá trị signature được dùng để kiểm tra tính chính xác của dữ liệu.

Cách tạo signature

  • Sử dụng thuật toán HMAC_SHA256 để tạo signature.
  • data tạo signature dạng: key1=value1&key2=value2... (key1: tên field, value1 = giá trị của key1).
  • Dữ liệu tạo signature sắp xếp theo key thứ tự alphabet.
  • Cấu trúc: hash_hmac("sha256", string $data , string $checksum_key)
Lưu ý:

Checksum Key được tạo ra sau khi tạo cổng thanh toán thành công

Code mẫu kiểm tra chính xác dữ liệu

    <?php
$checksum_key = "1a54716c8f0efb2744fb28b6e38b25da7f67a925d98bc1c18bd8faaecadd7675";
$webhookData = array(
"code" => "00",
"desc" => "success",
"success" => true,
"data" => array(
"orderCode" => 123,
"amount" => 3000,
"description" => "VQRIO123",
"accountNumber" => "12345678",
"reference" => "TF230204212323",
"transactionDateTime" => "2023-02-04 18:25:00",
"currency" => "VND",
"paymentLinkId" => "124c33293c43417ab7879e14c8d9eb18",
"code" => "00",
"desc" => "Thành công",
"counterAccountBankId" => "",
"counterAccountBankName" => "",
"counterAccountName" => "",
"counterAccountNumber" => "",
"virtualAccountName" => "",
"virtualAccountNumber" => ""
),
"signature" => "412e915d2871504ed31be63c8f62a149a4410d34c4c42affc9006ef9917eaa03"
);

function isValidData($transaction, $transaction_signature, $checksum_key)
{
ksort($transaction);
$transaction_str_arr = [];
foreach ($transaction as $key => $value) {
if (in_array($value, ["undefined", "null"]) || gettype($value) == "NULL") {
$value = "";
}

if (is_array($value)) {
$valueSortedElementObj = array_map(function ($ele) {
ksort($ele);
return $ele;
}, $value);
$value = json_encode($valueSortedElementObj, JSON_UNESCAPED_UNICODE);
}
$transaction_str_arr[] = $key . "=" . $value;
}
$transaction_str = implode("&", $transaction_str_arr);
dump($transaction_str);
$signature = hash_hmac("sha256", $transaction_str, $checksum_key);
dump($signature);
return $signature == $transaction_signature;
}

isValidData($webhookData['data'], $webhookData['signature'], $checksum_key);

Xem chi tiết về API tại đây.

code
required
string

Mã lỗi

desc
required
string

Thông tin lỗi

success
required
boolean
required
object
signature
required
string

Chữ kí để kiểm tra thông tin, chi tiết dữ liệu mẫu

{
  • "code": "00",
  • "desc": "success",
  • "success": true,
  • "data": {
    },
  • "signature": "8d8640d802576397a1ce45ebda7f835055768ac7ad2e0bfb77f9b8f12cca4c7f"
}

Kiểm tra thông tin bằng chữ ký (payouts)

Cách tạo và cấu trúc của signature

  • Sử dụng thuật toán HMAC_SHA256 để tạo signature.
  • data tạo signature dạng: key1=value1&key2=value2... (key1: tên field, value1 = encodeURI(giá trị của key1)).
  • Giá trị của key nếu là null hay undefined sẽ là chuỗi rỗng ""
  • Dữ liệu tạo signature sắp xếp theo key thứ tự alphabet, thứ tự các phần từ trong mảng giữ nguyên.
  • Cấu trúc: hash_hmac("sha256", string $data , string $checksum_key)
Lưu ý:

Checksum key được tạo ra khi tạo kênh chuyển tiền thành công và có thể thay đổi trên my.payos.vn

cảnh báo

Chữ ký dùng cho API payouts KHÁC với chữ ký dùng cho API payment-requests

Code mẫu kiểm tra chính xác dữ liệu

/**
* Deep sort object with optional array sorting
* @param obj Object data
* @param sortArrays Whether to sort arrays or maintain their order
* @returns Sorted object data
*/
function deepSortObj(obj, sortArrays = false) {
return Object.keys(obj)
.sort()
.reduce((acc, key) => {
const value = obj[key];

if (Array.isArray(value)) {
if (sortArrays) {
// Sort array elements
acc[key] = value
.map((item) =>
typeof item === 'object' && item !== null ? deepSortObj(item, sortArrays) : item,
)
.sort((a, b) => {
// Sort primitive values
if (typeof a !== 'object' && typeof b !== 'object') {
return String(a).localeCompare(String(b));
}
// For objects, sort by JSON string representation
return JSON.stringify(a).localeCompare(JSON.stringify(b));
});
} else {
// Maintain array order, but sort objects within arrays
acc[key] = value.map((item) =>
typeof item === 'object' && item !== null ? deepSortObj(item, sortArrays) : item,
);
}
} else if (typeof value === 'object' && value !== null) {
acc[key] = deepSortObj(value, sortArrays);
} else {
acc[key] = value;
}

return acc;
}, {});
}

/**
* Create HMAC SHA-256 signature using Web Crypto API (browser-compatible)
*
* @param {string} secretKey - Secret key for HMAC signature generation
* @param {object} jsonData - JSON object data to be signed
* @returns {Promise<string>} HMAC signature in hexadecimal format
*
* @example
* // Basic usage
* const signature = await createSignatureBrowser('secret', { name: 'John', age: 30 });
*/
async function createSignatureBrowser(secretKey, jsonData) {
const sortedData = deepSortObj(jsonData, false);

const queryString = Object.keys(sortedData)
.map((key) => {
let value = sortedData[key];

// Handle arrays by JSON stringify them
if (Array.isArray(value)) {
value = JSON.stringify(value);
}
// Handle nested objects
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
value = JSON.stringify(value);
}
// Handle null/undefined values
if (value === null || value === undefined) {
value = '';
}

return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`;
})
.join('&');

// Web Crypto API implementation
const encoder = new TextEncoder();
const keyData = encoder.encode(secretKey);
const messageData = encoder.encode(queryString);

const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);

const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
const hashArray = Array.from(new Uint8Array(signature));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');

return hashHex;
}

/**
* Verify signature using Web Crypto API
*
* @param {string} secretKey - Secret key used for signature generation
* @param {object} jsonData - JSON object data that was signed
* @param {string} expectedSignature - Expected signature to verify against
* @returns {Promise<boolean>} true if signatures match, false otherwise
*/
async function verifySignatureBrowser(secretKey, jsonData, expectedSignature) {
const computedSignature = await createSignatureBrowser(secretKey, jsonData);
return computedSignature.toLowerCase() === expectedSignature.toLowerCase();
}

const payoutChecksumKey = '6e91f59952acc8918c49c4a8e380136d66d1fbbf3375926840a8a7e434d4b325';

const expectedSignature = '34d500c4e17feaad8fab528ac3ae089353e276ca9fb4c6654c06ffdfbd88cc5d';

const payoutData = {
code: '00',
desc: 'success',
data: {
payouts: [
{
id: 'batch_8f9520b9341144f38b9f5fbfa317db8e',
referenceId: 'payout_1753061728877',
transactions: [
{
id: 'batch_txn_fdb348c0570a4cb99009da22f9504898',
referenceId: 'payout_1753061728877_0',
amount: 2000,
description: 'batch payout',
toBin: '970422',
toAccountNumber: '0123456789',
toAccountName: 'NGUYEN VAN A',
reference: '103269845',
transactionDatetime: '2025-07-21T08:35:40+07:00',
errorMessage: null,
errorCode: null,
state: 'SUCCEEDED',
},
{
id: 'batch_txn_d94d371c079f4fc0ab7154e1576629d8',
referenceId: 'payout_1753061728877_1',
amount: 2000,
description: 'batch payout',
toBin: '970422',
toAccountNumber: '0123456789',
toAccountName: 'NGUYEN VAN A',
reference: '103269846',
transactionDatetime: '2025-07-21T08:35:44+07:00',
errorMessage: null,
errorCode: null,
state: 'SUCCEEDED',
},
{
id: 'batch_txn_71eb922201f3442d93ec5a3c77347e9f',
referenceId: 'payout_1753061728877_2',
amount: 2000,
description: 'batch payout',
toBin: '970422',
toAccountNumber: '0123456789',
toAccountName: 'NGUYEN VAN A',
reference: '103269847',
transactionDatetime: '2025-07-21T08:35:47+07:00',
errorMessage: null,
errorCode: null,
state: 'SUCCEEDED',
},
],
category: ['salary'],
approvalState: 'COMPLETED',
createdAt: '2025-07-21T08:35:34+07:00',
},
],
pagination: {
limit: 10,
offset: 0,
total: 1,
count: 1,
hasMore: false,
},
},
};

(async () => {
const result = await verifySignatureBrowser(
payoutChecksumKey,
payoutData.data,
expectedSignature,
);

console.log('Signature payout validation result:', result);
})();