Skip to content

Xác thực chữ ký (Signature) ​

Collection Link sử dụng HMAC-SHA256 với secretKey để xác thực chuỗi dữ liệu đã ký. Kết quả là chuỗi hex viết thường, dài 64 ký tự.

HMAC không mã hóa nội dung: dữ liệu vẫn đọc được. Chữ ký chỉ bảo vệ các trường nằm trong chuỗi ký, không tự động bảo vệ toàn bộ JSON. Gửi request qua HTTPS và giữ secretKey ở backend.

Chọn đúng chuỗi ký ​

Thao tácTrường chữ kýHướng xử lý
Tạo đơn V1signatureMerchant ký request gửi Pay2S
Tạo đơn V2signatureKý thêm extraData và signatureVersion=2
Hủy đơnsignatureKý riêng với requestType=cancel
Mô phỏng thanh toán demosignatureKý riêng với requestType=simulatePayment
Nhận IPNm2signatureMerchant tính lại rồi so sánh

Không sắp xếp và ký tất cả khóa JSON. Dùng đúng danh sách và thứ tự trường của từng thao tác bên dưới. Các API dùng Bearer token hoặc pay2s-token có cơ chế xác thực riêng.

Quy tắc chung ​

  1. Lấy accessKey, partnerCode, secretKey từ cùng bộ khóa của tài khoản hoặc cửa hàng.
  2. Ghép tênTrường=giáTrị bằng dấu &, đúng thứ tự.
  3. Tính HMAC-SHA256 trên chuỗi UTF-8 với secretKey, xuất hex viết thường.
  4. Gửi kết quả trong trường chữ ký. Không gửi secretKey trong payload.
  • Không URL-encode chuỗi ký, không ký toàn bộ JSON, không thêm dấu cách hoặc xuống dòng.
  • Ký đúng giá trị gửi đi: URL, dấu / cuối URL, chữ hoa/thường và dấu tiếng Việt đều ảnh hưởng kết quả.
  • Dùng số tiền dạng 2200, không dùng 2.200, 2,200 hoặc 2.200 đ.
  • Với create, bankAccounts=Array là chuỗi cố định theo giao thức, không phải JSON danh sách ngân hàng. Payload vẫn gửi bankAccounts dạng mảng.
  • Không đưa signature / m2signature vào chuỗi ký của chính nó.

Tạo đơn V1 ​

V1 cũ không đưa extraData và signatureVersion vào chuỗi ký:

text
accessKey={accessKey}&amount={amount}&bankAccounts=Array&ipnUrl={ipnUrl}&orderId={orderId}&orderInfo={orderInfo}&partnerCode={partnerCode}&redirectUrl={redirectUrl}&requestId={requestId}&requestType={requestType}

Payload có thể bỏ signatureVersion hoặc gửi "1". Không được bỏ signature. Không cần thêm extraData cho luồng V1 cũ.

Tương thích V1

Backend còn chấp nhận biến thể V1 có extraData không rỗng được ký theo cấu trúc V2 bên dưới nhưng kết thúc bằng &signatureVersion=1. Metadata hóa đơn trong V1 không được dùng để đăng ký xuất hóa đơn. Khi tích hợp dữ liệu HĐĐT, dùng V2.

Tạo đơn V2 ​

Gửi signatureVersion: "2" và extraData không rỗng trong payload:

text
accessKey={accessKey}&amount={amount}&bankAccounts=Array&extraData={extraData}&ipnUrl={ipnUrl}&orderId={orderId}&orderInfo={orderInfo}&partnerCode={partnerCode}&redirectUrl={redirectUrl}&requestId={requestId}&requestType={requestType}&signatureVersion=2

extraData = Base64(JSON UTF-8 của metadata). Tạo giá trị này một lần, dùng cùng chuỗi để ký và gửi; không decode/re-encode sau khi ký. Xem schema tại Collection Link V2.

Các trường dễ nhầm

Tên đúng là extraData và signatureVersion, không phải extractData, extraxdata hay signature_version. Ký signatureVersion=2 nhưng bỏ trường này trong payload sẽ khiến backend mặc định V1 và chữ ký không khớp.

Node.js: tạo chữ ký V1/V2 ​

js
import { createHmac } from 'node:crypto';

function signCreate(p, secretKey) {
  const version = String(p.signatureVersion ?? '1');
  if (!['1', '2'].includes(version)) throw new Error('Unsupported version');
  if (version === '2' && !p.extraData) throw new Error('V2 requires extraData');
  let raw = `accessKey=${p.accessKey}&amount=${p.amount}&bankAccounts=Array`;
  if (version === '2') raw += `&extraData=${p.extraData}`;
  for (const field of ['ipnUrl', 'orderId', 'orderInfo', 'partnerCode', 'redirectUrl', 'requestId', 'requestType']) {
    raw += `&${field}=${p[field]}`;
  }
  if (version === '2') raw += '&signatureVersion=2';
  return createHmac('sha256', secretKey).update(raw, 'utf8').digest('hex');
}

// payload là body create; metadata theo schema Collection Link V2.
// Với V2, thực hiện trước khi ký:
// payload.signatureVersion = '2';
// payload.extraData = Buffer.from(JSON.stringify(metadata), 'utf8').toString('base64');
// payload.signature = signCreate(payload, process.env.PAY2S_SECRET_KEY);
// Gửi chính payload này, không sửa các trường đã ký.

PHP: tạo chữ ký V1/V2 ​

php
function signCreate(array $p, string $secretKey): string
{
    $version = (string)($p['signatureVersion'] ?? '1');
    if (!in_array($version, ['1', '2'], true)) {
        throw new InvalidArgumentException('Unsupported version');
    }
    if ($version === '2' && empty($p['extraData'])) {
        throw new InvalidArgumentException('V2 requires extraData');
    }
    $raw = 'accessKey=' . $p['accessKey'] . '&amount=' . $p['amount'] . '&bankAccounts=Array';
    if ($version === '2') $raw .= '&extraData=' . $p['extraData'];
    foreach (['ipnUrl', 'orderId', 'orderInfo', 'partnerCode', 'redirectUrl', 'requestId', 'requestType'] as $field) {
        $raw .= '&' . $field . '=' . $p[$field];
    }
    if ($version === '2') $raw .= '&signatureVersion=2';
    return hash_hmac('sha256', $raw, $secretKey);
}

// $payload['signatureVersion'] = '2';
// $payload['extraData'] = base64_encode(json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
// $payload['signature'] = signCreate($payload, getenv('PAY2S_SECRET_KEY'));

Hai hàm trên dùng chuỗi V1 cũ cho version 1 và chuỗi đầy đủ cho version 2. Có thể dùng SDK Node.js/PHP để tạo request V2 và xác thực IPN.

Hủy đơn hàng ​

Hủy đơn dùng cùng bộ khóa của create nhưng có chuỗi ký riêng:

text
accessKey={accessKey}&orderId={orderId}&partnerCode={partnerCode}&requestId={requestId}&requestType=cancel

orderId là mã gửi lúc tạo đơn; requestId là mã yêu cầu hủy. Không thêm amount, extraData, signatureVersion hoặc tái sử dụng chữ ký create.

Xem API hủy đơn hàng để lấy payload, script Postman và quy tắc xử lý đơn đã thanh toán.

Xác thực IPN ​

Để tự động kiểm thử với đơn demo, xem API mô phỏng thanh toán. API đó ghi nhận đơn rồi để worker gửi IPN; khác request Postman giả lập callback trực tiếp tới hệ thống của bạn.

IPN sử dụng m2signature, với chuỗi khác request create:

text
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}

Dùng accessKey cấu hình ở backend và giá trị nhận trong IPN cho các trường còn lại. Không thay orderInfo bằng giá trị từ request create: V2 có thể trả nội dung thanh toán do Pay2S sinh. Giữ đúng chuỗi extraData nhận được; trường rỗng vẫn phải có &extraData= trong chuỗi ký.

So sánh HMAC bằng hash_equals() (PHP) hoặc timingSafeEqual() (Node.js). Kiểm tra chữ ký nhận là 64 ký tự hex trước khi chuyển thành buffer. SDK có sẵn hàm xác thực IPN.

Chữ ký hợp lệ chưa đủ để giao hàng: đối chiếu partnerCode, orderId, số tiền với đơn đã lưu, kiểm tra resultCode và bảo đảm mỗi thanh toán chỉ xử lý một lần. Không xác nhận thanh toán chỉ dựa vào URL chuyển hướng của trình duyệt.

Kiểm tra thư viện HMAC ​

Bộ dữ liệu công khai dưới đây chỉ kiểm tra thuật toán, không phải request Pay2S hợp lệ:

text
secretKey: key
rawSignature: The quick brown fox jumps over the lazy dog
expectedSignature: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
js
createHmac('sha256', 'key')
  .update('The quick brown fox jumps over the lazy dog', 'utf8')
  .digest('hex');
php
hash_hmac('sha256', 'The quick brown fox jumps over the lazy dog', 'key');

Chẩn đoán lỗi chữ ký ​

Hiện tượngKiểm tra
V1 cũ ngừng khớpKhông tự thêm extraData / signatureVersion vào raw V1 cũ
V2 báo sai chữ kýPayload có signatureVersion: "2"; ký cùng extraData đã gửi
V2 có extraData: ""V2 yêu cầu metadata Base64 không rỗng
Sai khi thêm ngân hàngRaw create dùng bankAccounts=Array, không JSON.stringify mảng
Cùng dữ liệu nhưng chữ ký khácKiểm tra thứ tự trường, UTF-8, URL, xuống dòng, khoảng trắng, định dạng số
Ký đúng nhưng không xác thực đượcBộ accessKey/secretKey có thuộc tài khoản hoặc cửa hàng tương ứng partnerCode không
IPN luôn không khớpDùng m2signature và chuỗi IPN; không thay bằng chuỗi create
Hủy đơn không khớpKý mới với requestType=cancel

Debug bằng cách so sánh chuỗi trước HMAC ở môi trường kiểm thử. Không đưa secretKey, thông tin khách hàng hay request thật lên log công khai hoặc công cụ tạo HMAC bên ngoài.