Developer

Collection Link V2 – Tạo đơn có dữ liệu HĐĐT

✧ Mở bằng AI

Sao chép tài liệu, mở AI rồi dán vào cuộc trò chuyện.

ChatGPTClaudePerplexityGrok
≡ Xem Markdown

Collection Link V2 dùng để tạo liên kết thanh toán và gửi kèm thông tin cần thiết để Pay2S lập hóa đơn điện tử sau khi đơn được thanh toán thành công.

Đã có SDK Node.js/TypeScript và PHP để đóng gói chữ ký V2, metadata và xác thực IPN. Trang SDK có file tải, cách cài đặt và hướng dẫn dành cho AI coding assistant tích hợp đầy đủ luồng thanh toán.

Luồng xử lý

Với tài khoản và ngân hàng demo, có thể dùng API mô phỏng thanh toán để kiểm thử tự động sau bước tạo đơn.

Để hủy đơn chưa thanh toán, xem API hủy đơn hàng. API hủy dùng cùng bộ khóa với API create, với chuỗi ký riêng cho thao tác cancel.

  1. Website tạo Collection Link V2, kèm người mua và hàng hóa.
  2. Pay2S tự sinh orderInfo chuẩn, dùng mã này trong QR và trả về cùng payUrl.
  3. Khách hàng thanh toán.
  4. Khi thanh toán thành công, Pay2S gửi IPN đến ipnUrl và ghi nhận yêu cầu HĐĐT.
  5. Đơn xuất hiện trong Hàng chờ, hoặc được tự tạo nháp/phát hành theo cấu hình HĐĐT của tài khoản.

Tạo Collection Link thành công không đồng nghĩa hóa đơn đã được phát hành.

Điều kiện sử dụng HĐĐT

Tài khoản Pay2S cần:

Nếu không sử dụng Cửa hàng, Pay2S dùng kết nối và mẫu mặc định của tài khoản.

HTTP Request

Toàn bộ tham số Request

Trường Kiểu dữ liệu Bắt buộc Quy định và ý nghĩa
accessKey String Có Access Key của tài khoản Pay2S. Phải thuộc cùng tài khoản với partnerCode và secretKey dùng để ký.
partnerCode String Có Mã đối tác do Pay2S cấp. Lấy trong phần cấu hình tích hợp của tài khoản.
partnerName String Không Tên website, ứng dụng hoặc doanh nghiệp tạo đơn; tối đa 180 ký tự.
requestId String Có Mã duy nhất của mỗi lần gọi API; tối đa 80 ký tự. Chỉ dùng chữ, số và các ký tự ., _, :, -. Không tái sử dụng cho request khác.
amount Number hoặc String số Có Số tiền cần thanh toán bằng VND, không âm. Phải bằng tổng thanh toán của các dòng trong extraData.
orderId String Có Mã đơn hàng duy nhất tại hệ thống của đối tác; tối đa 80 ký tự. Chỉ dùng chữ, số và ., _, :, -.
orderInfo String Có Thông tin tham chiếu do đối tác gửi và dùng khi tạo chữ ký request. Với V2, Pay2S sẽ thay bằng một mã P2S... duy nhất trước khi tạo QR; hãy lấy orderInfo thực tế từ response.
orderType String Có Loại đơn hàng. Gửi cùng giá trị với requestType, hiện dùng pay2s.
bankAccounts Array Có Danh sách từ 1 đến 10 tài khoản ngân hàng nhận tiền đã liên kết với tài khoản Pay2S.
redirectUrl String URL Có URL HTTP/HTTPS chuyển khách hàng về sau khi thanh toán. Tối đa 500 ký tự.
ipnUrl String URL Có URL HTTP/HTTPS trên backend của đối tác dùng nhận kết quả thanh toán. Tối đa 500 ký tự.
requestType String Có Loại yêu cầu, hiện dùng pay2s. Trường này nằm trong chuỗi ký.
signatureVersion String Có Luôn gửi "2" khi sử dụng metadata hóa đơn điện tử.
extraData String Base64 Có JSON UTF-8 chứa invoiceType, customerInfo, items và invoiceOptions, sau đó mã hóa Base64 đúng một lần. Tối đa 32 KB.
signature String Có HMAC SHA-256 của chuỗi ký V2 bằng secretKey, kết quả hex chữ thường.
lang String Không Ngôn ngữ giao diện thanh toán, ví dụ vi; nếu bỏ trống Pay2S dùng ngôn ngữ mặc định của tài khoản.

Cấu trúc bankAccounts

Mỗi phần tử trong bankAccounts gồm:

Trường Kiểu Bắt buộc Mô tả
account_number String Có Số tài khoản hoặc số tài khoản định danh đã liên kết trên Pay2S, từ 4 đến 40 ký tự.
bank_id String Có Mã ngân hàng, ví dụ ACB, VCB, MBB; từ 2 đến 20 chữ hoặc số.

Ví dụ:

[
  {
    "account_number": "P2S99999999",
    "bank_id": "ACB"
  }
]

Cấu trúc metadata trước khi Base64

{
  "invoiceType": "vat",
  "customerInfo": {
    "buyerContactName": "NGUYỄN VĂN A",
    "buyerCompanyName": "CÔNG TY TNHH ABC",
    "taxCode": "0312345678",
    "citizenId": "",
    "address": "123 Nguyễn Huệ, TP.HCM",
    "email": "[email protected]",
    "phone": "0901234567"
  },
  "items": [
    {
      "itemCode": "DV001",
      "itemGroupCode": "API_GROUP_SOFTWARE",
      "externalItemId": "ORDER_LINE_001",
      "sourceProductName": "Gói phần mềm Pro",
      "itemName": "Dịch vụ phần mềm",
      "unit": "Tháng",
      "quantity": 1,
      "unitPrice": 100000,
      "taxRate": 10
    }
  ],
  "invoiceOptions": {
    "requested": true,
    "buyerNotTakingInvoice": false,
    "paymentMethod": "Chuyển khoản",
    "note": "",
    "source": "api"
  }
}

Metadata gồm bốn nhóm:

Nhóm Bắt buộc Ý nghĩa
invoiceType Có Dùng vat cho đơn có yêu cầu HĐĐT; regular cho đơn thông thường; none khi không sử dụng hóa đơn
customerInfo Có Thông tin cá nhân hoặc doanh nghiệp mua hàng
items Có Danh sách tối đa 50 dòng hàng hóa, dịch vụ
invoiceOptions Có Yêu cầu xuất hóa đơn, nguồn tích hợp, hình thức thanh toán và ghi chú

Để đơn xuất hiện trong luồng HĐĐT, sử dụng invoiceType="vat" đồng thời đặt invoiceOptions.requested=true.

invoiceType trong API tạo thanh toán chỉ thể hiện đơn có tham gia luồng HĐĐT hay không. Không gửi invoiceType="sales". Pay2S xác định hóa đơn GTGT hay hóa đơn bán hàng từ mẫu hóa đơn đã được gán cho cửa hàng hoặc kết nối.

Thông tin người mua

Trường Mô tả
buyerContactName Họ tên người trực tiếp mua hàng
buyerCompanyName Tên doanh nghiệp hoặc tổ chức
taxCode Mã số thuế của đơn vị mua hàng
citizenId CCCD của người mua nếu cần
address Địa chỉ đầy đủ; không chỉ gửi mã quốc gia như VN
email Email nhận hóa đơn
phone Số điện thoại người mua

Thông tin hợp lệ được xác định như sau:

Nếu không đủ thông tin, Pay2S sử dụng Bán cho người tiêu dùng để tránh phát hành thông tin người mua không đầy đủ.

Tra cứu mã số thuế và thứ tự ưu tiên

Website có thể gọi API tra cứu mã số thuế từ backend để điền customerInfo.taxCode, buyerCompanyName, address trước khi tạo đơn. API tra cứu dùng header pay2s-token = Base64(Secret Key); không dùng chữ ký Collection Link V2 cho endpoint này.

Nếu tài khoản đã bật Tự động kiểm tra mã số thuế, Pay2S kiểm tra taxCode trước khi tự động tạo nháp hoặc phát hành:

Xem quy tắc đầy đủ tại Tra cứu mã số thuế và dữ liệu người mua.

Người mua không lấy hóa đơn

Đặt:

{
  "requested": true,
  "buyerNotTakingInvoice": true
}

Pay2S sẽ dùng Bán cho người tiêu dùng. Danh sách hàng hóa vẫn phải được gửi đầy đủ.

Danh sách hàng hóa

Trường Bắt buộc Mô tả
itemName Có Tên hàng hóa hoặc dịch vụ; dùng làm giá trị dự phòng khi Quy tắc sản phẩm khớp theo tên
itemCode Không Mã ổn định để khớp quy tắc hàng hóa
itemGroupCode Không Mã nhóm để áp dụng chung một quy tắc
externalItemId Không ID phục vụ đối chiếu với hệ thống nguồn
sourceProductName Không Tên nguyên bản trên website; được ưu tiên đối chiếu khi Quy tắc sản phẩm khớp theo tên
unit Không Đơn vị tính, ví dụ Tháng, Cái, Lần
quantity Có Số lượng lớn hơn 0
unitPrice Có Đơn giá trước thuế, không âm
taxRate Có -2, -1, 0, 3.5, 5, 8 hoặc 10
vatReductionEligible Không true nếu dòng hàng thuộc diện giảm thuế GTGT theo Nghị quyết 204/2025/QH15; mặc định false
directVatRate Có điều kiện Tỷ lệ tính thuế GTGT trực tiếp trên doanh thu: 1, 2, 3 hoặc 5; bắt buộc khi vatReductionEligible=true

Các giá trị thuế đặc biệt:

Kiểm tra tổng tiền

Tiền hàng = quantity × unitPrice
Tiền thuế = Tiền hàng × taxRate / 100
Tổng thanh toán = Tổng tiền hàng + Tổng tiền thuế

Với thuế -1 hoặc -2, tiền thuế bằng 0. Tổng tất cả dòng hàng phải khớp amount của đơn.

Ví dụ unitPrice=100000, quantity=1, taxRate=10 thì amount phải bằng 110000.

Hóa đơn GTGT và hóa đơn bán hàng

Loại hóa đơn được lấy từ mẫu đã gán trên Pay2S, không lấy từ một trường do client tự khai báo:

directVatRate là tỷ lệ tính thuế GTGT trực tiếp trên doanh thu, không phải thuế suất hiển thị của hóa đơn GTGT. Pay2S tính số tiền được giảm của từng dòng như sau:

Tiền hàng trước giảm = quantity × unitPrice
Tiền được giảm = Tiền hàng trước giảm × directVatRate / 100 × 20%
Tiền thanh toán của dòng = Tiền hàng trước giảm - Tiền được giảm

Ví dụ một đơn có một món được giảm và một món không được giảm:

{
  "items": [
    {
      "itemCode": "MON_A",
      "itemName": "Món A",
      "unit": "Phần",
      "quantity": 1,
      "unitPrice": 100000,
      "taxRate": 0,
      "vatReductionEligible": true,
      "directVatRate": 5
    },
    {
      "itemCode": "MON_B",
      "itemName": "Món B",
      "unit": "Phần",
      "quantity": 1,
      "unitPrice": 50000,
      "taxRate": 0,
      "vatReductionEligible": false
    }
  ]
}

Món A được giảm 1.000 đồng, món B không giảm nên amount của request phải là 149000. unitPrice phải là giá trước khoản giảm theo Nghị quyết 204; không gửi giá đã giảm rồi yêu cầu Pay2S giảm thêm lần nữa.

Nếu hệ thống nguồn hiện đang tách thuế và đã gửi taxRate thực tế, client cũ có thể tiếp tục gửi như trước. Khi mẫu đã gán là hóa đơn bán hàng, Pay2S gộp phần thuế nguồn vào giá bán để giữ đúng số tiền giao dịch.

Tùy chọn hóa đơn

Trường Mô tả
requested true khi đơn này cần xuất HĐĐT
buyerNotTakingInvoice true khi người mua không lấy hoặc không cung cấp đủ thông tin
paymentMethod Ví dụ Chuyển khoản, mặc định có thể dùng TM/CK
note Ghi chú hóa đơn, tối đa 500 ký tự
source Nguồn ổn định như api, whmcs, woocommerce, hostbill

Không đặt requested=true cho mọi đơn nếu chỉ một số khách yêu cầu hóa đơn.

Mã hóa extraData

Thực hiện đúng thứ tự:

  1. Chuyển metadata thành JSON UTF-8.
  2. Base64 chuỗi JSON đúng một lần.
  3. Dùng chính chuỗi Base64 đó trong payload và chuỗi ký.

Ví dụ Node.js:

const extraData = Buffer
  .from(JSON.stringify(metadata), 'utf8')
  .toString('base64');

Ví dụ PHP:

$extraData = base64_encode(
    json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);

Tạo chữ ký V2

Ghép đúng thứ tự dưới đây, không URL encode extraData trước khi ký:

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

Tạo chữ ký bằng HMAC SHA-256 với secretKey, xuất kết quả dạng hex chữ thường.

Request mẫu đầy đủ

{
  "accessKey": "YOUR_KEY",
  "partnerCode": "PAY2S7EPF0SB1ZP27W71",
  "partnerName": "Cửa hàng trải nghiệm",
  "requestId": "DEVd33eb13817916599723201625c7",
  "amount": 250000,
  "orderId": "DEVd33eb13817916599723201625c7",
  "orderInfo": "TT817916599723201625c7",
  "bankAccounts": [
    {
      "account_number": "P2S99999999",
      "bank_id": "ACB"
    }
  ],
  "redirectUrl": "https://developer.example.com/demos/?mode=live",
  "ipnUrl": "https://developer.example.com/sandbox/live-receiver/ipn",
  "requestType": "pay2s",
  "signature": "<SIGNATURE>",
  "signatureVersion": "2",
  "extraData": "eyJpbnZvaWNlVHlwZSI6InZhdCIsImN1c3RvbWVySW5mbyI6eyJidXllckNvbnRhY3ROYW1lIjoiQsOhbiBjaG8gbmfGsOG7nWkgdGnDqnUgZMO5bmcifSwiaXRlbXMiOlt7Iml0ZW1OYW1lIjoiU+G6o24gcGjhuqltIGRlbW8iLCJxdWFudGl0eSI6MSwidW5pdFByaWNlIjoyNTAwMDAsInRheFJhdGUiOjB9XSwiaW52b2ljZU9wdGlvbnMiOnsicmVxdWVzdGVkIjp0cnVlLCJidXllck5vdFRha2luZ0ludm9pY2UiOnRydWV9fQ=="
}

orderInfo do Pay2S tự sinh

Trong request V2, đối tác vẫn gửi và ký trường orderInfo để request có cấu trúc tương thích. Sau khi chữ ký hợp lệ, Pay2S tự thay giá trị này bằng mã thanh toán chuẩn, ví dụ:

P2S8A4F21C09D7B

Mã do Pay2S sinh:

Response tạo đơn trả lại orderInfo thực tế:

{
  "partnerCode": "PAY2S7EPF0SB1ZP27W71",
  "requestId": "DEVd33eb13817916599723201625c7",
  "orderId": "DEVd33eb13817916599723201625c7",
  "orderInfo": "TT817916599723201625c7",
  "amount": 250000,
  "message": "Thành công",
  "lang": "vi",
  "resultCode": 0,
  "qrList": [
    {
      "bank_id": "ACB",
      "account_number": "P2S99999999",
      "account_name": "TAI KHOAN DEMO ACB",
      "qrCode": "<QRCODE_OMITTED>",
      "qrData": "00020101021238550010A000000727012500069704160111P2S999999990208QRIBFTTA52040000530370454062500005802VN62260822TT817916599723201625c7630495CF",
      "qrUrl": "https://sandbox-payment.pay2s.vn/quicklink/ACB/P2S99999999/TAI%20KHOAN%20DEMO%20ACB?amount=250000&memo=TT817916599723201625c7&is_mask=0"
    }
  ],
  "payUrl": "https://sandbox-payment.pay2s.vn/v2/gateway/pay?t=YOUR_KEY",
  "storeId": null,
  "credentialScope": "owner",
  "isDemo": true,
  "environment": "demo"
}

💻 Code mẫu hoàn chỉnh

Các ví dụ dưới đây đều thực hiện cùng một quy trình: tạo metadata HĐĐT, mã hóa Base64 UTF-8, ký V2 và gửi request JSON đến Pay2S. Hãy đặt khóa bí mật trong biến môi trường trên backend.

<?php

$endpoint = 'https://payment.pay2s.vn/v1/gateway/api/create';
$accessKey = getenv('PAY2S_ACCESS_KEY');
$secretKey = getenv('PAY2S_SECRET_KEY');
$partnerCode = getenv('PAY2S_PARTNER_CODE');

$requestId = (string) round(microtime(true) * 1000);
$orderId = 'ORDER-' . $requestId;
$amount = '110000';
$orderInfo = 'TT' . $requestId;
$requestType = 'pay2s';
$redirectUrl = 'https://merchant.example/return';
$ipnUrl = 'https://merchant.example/pay2s/ipn';
$bankAccounts = [['account_number' => '99999999', 'bank_id' => 'ACB']];

$metadata = [
    'invoiceType' => 'vat',
    'customerInfo' => [
        'buyerContactName' => 'NGUYỄN VĂN A',
        'buyerCompanyName' => 'CÔNG TY TNHH ABC',
        'taxCode' => '0312345678',
        'citizenId' => '',
        'address' => '123 Nguyễn Huệ, TP.HCM',
        'email' => '[email protected]',
        'phone' => '0901234567',
    ],
    'items' => [[
        'itemCode' => 'DV001',
        'itemGroupCode' => 'API_GROUP_SOFTWARE',
        'sourceProductName' => 'Gói phần mềm Pro',
        'itemName' => 'Dịch vụ phần mềm',
        'unit' => 'Tháng',
        'quantity' => 1,
        'unitPrice' => 100000,
        'taxRate' => 10,
    ]],
    'invoiceOptions' => [
        'requested' => true,
        'buyerNotTakingInvoice' => false,
        'paymentMethod' => 'Chuyển khoản',
        'note' => '',
        'source' => 'api',
    ],
];

$metadataJson = json_encode(
    $metadata,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
);
$extraData = base64_encode($metadataJson);

$rawSignature = "accessKey={$accessKey}"
    . "&amount={$amount}"
    . '&bankAccounts=Array'
    . "&extraData={$extraData}"
    . "&ipnUrl={$ipnUrl}"
    . "&orderId={$orderId}"
    . "&orderInfo={$orderInfo}"
    . "&partnerCode={$partnerCode}"
    . "&redirectUrl={$redirectUrl}"
    . "&requestId={$requestId}"
    . "&requestType={$requestType}"
    . '&signatureVersion=2';

$payload = [
    'accessKey' => $accessKey,
    'partnerCode' => $partnerCode,
    'partnerName' => 'CÔNG TY TNHH ABC',
    'requestId' => $requestId,
    'amount' => $amount,
    'orderId' => $orderId,
    'orderInfo' => $orderInfo,
    'orderType' => $requestType,
    'bankAccounts' => $bankAccounts,
    'redirectUrl' => $redirectUrl,
    'ipnUrl' => $ipnUrl,
    'requestType' => $requestType,
    'signatureVersion' => '2',
    'extraData' => $extraData,
    'signature' => hash_hmac('sha256', $rawSignature, $secretKey),
];

$ch = curl_init($endpoint);
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=UTF-8'],
    CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);

$response = curl_exec($ch);
if ($response === false) {
    throw new RuntimeException(curl_error($ch));
}
curl_close($ch);

echo $response;
const crypto = require('crypto');
const axios = require('axios');

const endpoint = 'https://payment.pay2s.vn/v1/gateway/api/create';
const accessKey = process.env.PAY2S_ACCESS_KEY;
const secretKey = process.env.PAY2S_SECRET_KEY;
const partnerCode = process.env.PAY2S_PARTNER_CODE;

const requestId = Date.now().toString();
const orderId = `ORDER-${requestId}`;
const amount = '110000';
const orderInfo = `TT${requestId}`;
const requestType = 'pay2s';
const redirectUrl = 'https://merchant.example/return';
const ipnUrl = 'https://merchant.example/pay2s/ipn';
const bankAccounts = [{ account_number: '99999999', bank_id: 'ACB' }];

const metadata = {
  invoiceType: 'vat',
  customerInfo: {
    buyerContactName: 'NGUYỄN VĂN A',
    buyerCompanyName: 'CÔNG TY TNHH ABC',
    taxCode: '0312345678',
    citizenId: '',
    address: '123 Nguyễn Huệ, TP.HCM',
    email: '[email protected]',
    phone: '0901234567'
  },
  items: [{
    itemCode: 'DV001',
    itemGroupCode: 'API_GROUP_SOFTWARE',
    externalItemId: 'ORDER_LINE_001',
    sourceProductName: 'Gói phần mềm Pro',
    itemName: 'Dịch vụ phần mềm',
    unit: 'Tháng',
    quantity: 1,
    unitPrice: 100000,
    taxRate: 10
  }],
  invoiceOptions: {
    requested: true,
    buyerNotTakingInvoice: false,
    paymentMethod: 'Chuyển khoản',
    note: '',
    source: 'api'
  }
};

const extraData = Buffer.from(JSON.stringify(metadata), 'utf8').toString('base64');
const rawSignature =
  `accessKey=${accessKey}` +
  `&amount=${amount}` +
  `&bankAccounts=Array` +
  `&extraData=${extraData}` +
  `&ipnUrl=${ipnUrl}` +
  `&orderId=${orderId}` +
  `&orderInfo=${orderInfo}` +
  `&partnerCode=${partnerCode}` +
  `&redirectUrl=${redirectUrl}` +
  `&requestId=${requestId}` +
  `&requestType=${requestType}` +
  `&signatureVersion=2`;

const signature = crypto
  .createHmac('sha256', secretKey)
  .update(rawSignature, 'utf8')
  .digest('hex');

const payload = {
  accessKey,
  partnerCode,
  partnerName: 'CÔNG TY TNHH ABC',
  requestId,
  amount,
  orderId,
  orderInfo,
  orderType: requestType,
  bankAccounts,
  redirectUrl,
  ipnUrl,
  requestType,
  signatureVersion: '2',
  extraData,
  signature
};

axios.post(endpoint, payload, {
  headers: { 'Content-Type': 'application/json; charset=UTF-8' }
}).then(({ data }) => {
  console.log(data);
});
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

const string endpoint = "https://payment.pay2s.vn/v1/gateway/api/create";
string accessKey = Environment.GetEnvironmentVariable("PAY2S_ACCESS_KEY")!;
string secretKey = Environment.GetEnvironmentVariable("PAY2S_SECRET_KEY")!;
string partnerCode = Environment.GetEnvironmentVariable("PAY2S_PARTNER_CODE")!;

string requestId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
string orderId = $"ORDER-{requestId}";
string amount = "110000";
string orderInfo = $"TT{requestId}";
string requestType = "pay2s";
string redirectUrl = "https://merchant.example/return";
string ipnUrl = "https://merchant.example/pay2s/ipn";
var bankAccounts = new[] { new { account_number = "99999999", bank_id = "ACB" } };

var metadata = new
{
    invoiceType = "vat",
    customerInfo = new
    {
        buyerContactName = "NGUYỄN VĂN A",
        buyerCompanyName = "CÔNG TY TNHH ABC",
        taxCode = "0312345678",
        citizenId = "",
        address = "123 Nguyễn Huệ, TP.HCM",
        email = "[email protected]",
        phone = "0901234567"
    },
    items = new[]
    {
        new
        {
            itemCode = "DV001",
            itemGroupCode = "API_GROUP_SOFTWARE",
            sourceProductName = "Gói phần mềm Pro",
            itemName = "Dịch vụ phần mềm",
            unit = "Tháng",
            quantity = 1,
            unitPrice = 100000,
            taxRate = 10
        }
    },
    invoiceOptions = new
    {
        requested = true,
        buyerNotTakingInvoice = false,
        paymentMethod = "Chuyển khoản",
        note = "",
        source = "api"
    }
};

string metadataJson = JsonSerializer.Serialize(metadata);
string extraData = Convert.ToBase64String(Encoding.UTF8.GetBytes(metadataJson));
string rawSignature = $"accessKey={accessKey}"
    + $"&amount={amount}"
    + "&bankAccounts=Array"
    + $"&extraData={extraData}"
    + $"&ipnUrl={ipnUrl}"
    + $"&orderId={orderId}"
    + $"&orderInfo={orderInfo}"
    + $"&partnerCode={partnerCode}"
    + $"&redirectUrl={redirectUrl}"
    + $"&requestId={requestId}"
    + $"&requestType={requestType}"
    + "&signatureVersion=2";

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
string signature = Convert.ToHexString(
    hmac.ComputeHash(Encoding.UTF8.GetBytes(rawSignature))
).ToLowerInvariant();

var payload = new
{
    accessKey,
    partnerCode,
    partnerName = "CÔNG TY TNHH ABC",
    requestId,
    amount,
    orderId,
    orderInfo,
    orderType = requestType,
    bankAccounts,
    redirectUrl,
    ipnUrl,
    requestType,
    signatureVersion = "2",
    extraData,
    signature
};

using var http = new HttpClient();
var response = await http.PostAsJsonAsync(endpoint, payload);
Console.WriteLine(await response.Content.ReadAsStringAsync());
import base64
import hashlib
import hmac
import json
import os
import time

import requests

endpoint = "https://payment.pay2s.vn/v1/gateway/api/create"
access_key = os.environ["PAY2S_ACCESS_KEY"]
secret_key = os.environ["PAY2S_SECRET_KEY"]
partner_code = os.environ["PAY2S_PARTNER_CODE"]

request_id = str(int(time.time() * 1000))
order_id = f"ORDER-{request_id}"
amount = "110000"
order_info = f"TT{request_id}"
request_type = "pay2s"
redirect_url = "https://merchant.example/return"
ipn_url = "https://merchant.example/pay2s/ipn"
bank_accounts = [{"account_number": "99999999", "bank_id": "ACB"}]

metadata = {
    "invoiceType": "vat",
    "customerInfo": {
        "buyerContactName": "NGUYỄN VĂN A",
        "buyerCompanyName": "CÔNG TY TNHH ABC",
        "taxCode": "0312345678",
        "citizenId": "",
        "address": "123 Nguyễn Huệ, TP.HCM",
        "email": "[email protected]",
        "phone": "0901234567",
    },
    "items": [{
        "itemCode": "DV001",
        "itemGroupCode": "API_GROUP_SOFTWARE",
        "sourceProductName": "Gói phần mềm Pro",
        "itemName": "Dịch vụ phần mềm",
        "unit": "Tháng",
        "quantity": 1,
        "unitPrice": 100000,
        "taxRate": 10,
    }],
    "invoiceOptions": {
        "requested": True,
        "buyerNotTakingInvoice": False,
        "paymentMethod": "Chuyển khoản",
        "note": "",
        "source": "api",
    },
}

metadata_json = json.dumps(
    metadata, ensure_ascii=False, separators=(",", ":")
).encode("utf-8")
extra_data = base64.b64encode(metadata_json).decode("ascii")
raw_signature = (
    f"accessKey={access_key}"
    f"&amount={amount}"
    "&bankAccounts=Array"
    f"&extraData={extra_data}"
    f"&ipnUrl={ipn_url}"
    f"&orderId={order_id}"
    f"&orderInfo={order_info}"
    f"&partnerCode={partner_code}"
    f"&redirectUrl={redirect_url}"
    f"&requestId={request_id}"
    f"&requestType={request_type}"
    "&signatureVersion=2"
)
signature = hmac.new(
    secret_key.encode("utf-8"), raw_signature.encode("utf-8"), hashlib.sha256
).hexdigest()

payload = {
    "accessKey": access_key,
    "partnerCode": partner_code,
    "partnerName": "CÔNG TY TNHH ABC",
    "requestId": request_id,
    "amount": amount,
    "orderId": order_id,
    "orderInfo": order_info,
    "orderType": request_type,
    "bankAccounts": bank_accounts,
    "redirectUrl": redirect_url,
    "ipnUrl": ipn_url,
    "requestType": request_type,
    "signatureVersion": "2",
    "extraData": extra_data,
    "signature": signature,
}

response = requests.post(endpoint, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
// Yêu cầu Java 11+ và thư viện Gson.
import com.google.gson.Gson;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Pay2SCollectionLinkV2 {
    private static String hex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte value : bytes) result.append(String.format("%02x", value));
        return result.toString();
    }

    public static void main(String[] args) throws Exception {
        String endpoint = "https://payment.pay2s.vn/v1/gateway/api/create";
        String accessKey = System.getenv("PAY2S_ACCESS_KEY");
        String secretKey = System.getenv("PAY2S_SECRET_KEY");
        String partnerCode = System.getenv("PAY2S_PARTNER_CODE");
        String requestId = String.valueOf(Instant.now().toEpochMilli());
        String orderId = "ORDER-" + requestId;
        String amount = "110000";
        String orderInfo = "TT" + requestId;
        String requestType = "pay2s";
        String redirectUrl = "https://merchant.example/return";
        String ipnUrl = "https://merchant.example/pay2s/ipn";
        List<Map<String, Object>> bankAccounts = List.of(
            Map.of("account_number", "99999999", "bank_id", "ACB")
        );

        Map<String, Object> metadata = Map.of(
            "invoiceType", "vat",
            "customerInfo", Map.of(
                "buyerContactName", "NGUYỄN VĂN A",
                "buyerCompanyName", "CÔNG TY TNHH ABC",
                "taxCode", "0312345678",
                "citizenId", "",
                "address", "123 Nguyễn Huệ, TP.HCM",
                "email", "[email protected]",
                "phone", "0901234567"
            ),
            "items", List.of(Map.of(
                "itemCode", "DV001",
                "itemGroupCode", "API_GROUP_SOFTWARE",
                "sourceProductName", "Gói phần mềm Pro",
                "itemName", "Dịch vụ phần mềm",
                "unit", "Tháng",
                "quantity", 1,
                "unitPrice", 100000,
                "taxRate", 10
            )),
            "invoiceOptions", Map.of(
                "requested", true,
                "buyerNotTakingInvoice", false,
                "paymentMethod", "Chuyển khoản",
                "note", "",
                "source", "api"
            )
        );

        Gson gson = new Gson();
        String extraData = Base64.getEncoder().encodeToString(
            gson.toJson(metadata).getBytes(StandardCharsets.UTF_8)
        );
        String rawSignature = "accessKey=" + accessKey
            + "&amount=" + amount
            + "&bankAccounts=Array"
            + "&extraData=" + extraData
            + "&ipnUrl=" + ipnUrl
            + "&orderId=" + orderId
            + "&orderInfo=" + orderInfo
            + "&partnerCode=" + partnerCode
            + "&redirectUrl=" + redirectUrl
            + "&requestId=" + requestId
            + "&requestType=" + requestType
            + "&signatureVersion=2";

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        String signature = hex(mac.doFinal(rawSignature.getBytes(StandardCharsets.UTF_8)));

        Map<String, Object> payload = Map.ofEntries(
            Map.entry("accessKey", accessKey),
            Map.entry("partnerCode", partnerCode),
            Map.entry("partnerName", "CÔNG TY TNHH ABC"),
            Map.entry("requestId", requestId),
            Map.entry("amount", amount),
            Map.entry("orderId", orderId),
            Map.entry("orderInfo", orderInfo),
            Map.entry("orderType", requestType),
            Map.entry("bankAccounts", bankAccounts),
            Map.entry("redirectUrl", redirectUrl),
            Map.entry("ipnUrl", ipnUrl),
            Map.entry("requestType", requestType),
            Map.entry("signatureVersion", "2"),
            Map.entry("extraData", extraData),
            Map.entry("signature", signature)
        );

        HttpRequest request = HttpRequest.newBuilder(URI.create(endpoint))
            .header("Content-Type", "application/json; charset=UTF-8")
            .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(payload)))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient().send(
            request, HttpResponse.BodyHandlers.ofString()
        );
        System.out.println(response.body());
    }
}
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"time"
)

func main() {
	endpoint := "https://payment.pay2s.vn/v1/gateway/api/create"
	accessKey := os.Getenv("PAY2S_ACCESS_KEY")
	secretKey := os.Getenv("PAY2S_SECRET_KEY")
	partnerCode := os.Getenv("PAY2S_PARTNER_CODE")
	requestID := strconv.FormatInt(time.Now().UnixMilli(), 10)
	orderID := "ORDER-" + requestID
	amount := "110000"
	orderInfo := "TT" + requestID
	requestType := "pay2s"
	redirectURL := "https://merchant.example/return"
	ipnURL := "https://merchant.example/pay2s/ipn"
	bankAccounts := []map[string]any{{"account_number": "99999999", "bank_id": "ACB"}}

	metadata := map[string]any{
		"invoiceType": "vat",
		"customerInfo": map[string]any{
			"buyerContactName": "NGUYỄN VĂN A",
			"buyerCompanyName": "CÔNG TY TNHH ABC",
			"taxCode": "0312345678",
			"citizenId": "",
			"address": "123 Nguyễn Huệ, TP.HCM",
			"email": "[email protected]",
			"phone": "0901234567",
		},
		"items": []map[string]any{{
			"itemCode": "DV001", "itemGroupCode": "API_GROUP_SOFTWARE",
			"sourceProductName": "Gói phần mềm Pro",
			"itemName": "Dịch vụ phần mềm", "unit": "Tháng",
			"quantity": 1, "unitPrice": 100000, "taxRate": 10,
		}},
		"invoiceOptions": map[string]any{
			"requested": true, "buyerNotTakingInvoice": false,
			"paymentMethod": "Chuyển khoản", "note": "", "source": "api",
		},
	}

	metadataJSON, _ := json.Marshal(metadata)
	extraData := base64.StdEncoding.EncodeToString(metadataJSON)
	rawSignature := "accessKey=" + accessKey +
		"&amount=" + amount +
		"&bankAccounts=Array" +
		"&extraData=" + extraData +
		"&ipnUrl=" + ipnURL +
		"&orderId=" + orderID +
		"&orderInfo=" + orderInfo +
		"&partnerCode=" + partnerCode +
		"&redirectUrl=" + redirectURL +
		"&requestId=" + requestID +
		"&requestType=" + requestType +
		"&signatureVersion=2"

	mac := hmac.New(sha256.New, []byte(secretKey))
	mac.Write([]byte(rawSignature))
	signature := hex.EncodeToString(mac.Sum(nil))
	payload := map[string]any{
		"accessKey": accessKey, "partnerCode": partnerCode,
		"partnerName": "CÔNG TY TNHH ABC", "requestId": requestID,
		"amount": amount, "orderId": orderID, "orderInfo": orderInfo,
		"orderType": requestType, "bankAccounts": bankAccounts,
		"redirectUrl": redirectURL, "ipnUrl": ipnURL,
		"requestType": requestType, "signatureVersion": "2",
		"extraData": extraData, "signature": signature,
	}

	body, _ := json.Marshal(payload)
	response, err := http.Post(endpoint, "application/json; charset=UTF-8", bytes.NewReader(body))
	if err != nil { panic(err) }
	defer response.Body.Close()
	result, _ := io.ReadAll(response.Body)
	fmt.Println(string(result))
}
require 'base64'
require 'json'
require 'net/http'
require 'openssl'
require 'time'

endpoint = URI('https://payment.pay2s.vn/v1/gateway/api/create')
access_key = ENV.fetch('PAY2S_ACCESS_KEY')
secret_key = ENV.fetch('PAY2S_SECRET_KEY')
partner_code = ENV.fetch('PAY2S_PARTNER_CODE')

request_id = (Time.now.to_f * 1000).to_i.to_s
order_id = "ORDER-#{request_id}"
amount = '110000'
order_info = "TT#{request_id}"
request_type = 'pay2s'
redirect_url = 'https://merchant.example/return'
ipn_url = 'https://merchant.example/pay2s/ipn'
bank_accounts = [{ account_number: '99999999', bank_id: 'ACB' }]

metadata = {
  invoiceType: 'vat',
  customerInfo: {
    buyerContactName: 'NGUYỄN VĂN A',
    buyerCompanyName: 'CÔNG TY TNHH ABC',
    taxCode: '0312345678',
    citizenId: '',
    address: '123 Nguyễn Huệ, TP.HCM',
    email: '[email protected]',
    phone: '0901234567'
  },
  items: [{
    itemCode: 'DV001',
    itemGroupCode: 'API_GROUP_SOFTWARE',
    sourceProductName: 'Gói phần mềm Pro',
    itemName: 'Dịch vụ phần mềm',
    unit: 'Tháng',
    quantity: 1,
    unitPrice: 100000,
    taxRate: 10
  }],
  invoiceOptions: {
    requested: true,
    buyerNotTakingInvoice: false,
    paymentMethod: 'Chuyển khoản',
    note: '',
    source: 'api'
  }
}

extra_data = Base64.strict_encode64(JSON.generate(metadata).encode('UTF-8'))
raw_signature = "accessKey=#{access_key}" \
  "&amount=#{amount}" \
  '&bankAccounts=Array' \
  "&extraData=#{extra_data}" \
  "&ipnUrl=#{ipn_url}" \
  "&orderId=#{order_id}" \
  "&orderInfo=#{order_info}" \
  "&partnerCode=#{partner_code}" \
  "&redirectUrl=#{redirect_url}" \
  "&requestId=#{request_id}" \
  "&requestType=#{request_type}" \
  '&signatureVersion=2'
signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_signature)

payload = {
  accessKey: access_key,
  partnerCode: partner_code,
  partnerName: 'CÔNG TY TNHH ABC',
  requestId: request_id,
  amount: amount,
  orderId: order_id,
  orderInfo: order_info,
  orderType: request_type,
  bankAccounts: bank_accounts,
  redirectUrl: redirect_url,
  ipnUrl: ipn_url,
  requestType: request_type,
  signatureVersion: '2',
  extraData: extra_data,
  signature: signature
}

request = Net::HTTP::Post.new(endpoint)
request['Content-Type'] = 'application/json; charset=UTF-8'
request.body = JSON.generate(payload)
response = Net::HTTP.start(endpoint.host, endpoint.port, use_ssl: true) do |http|
  http.request(request)
end
puts response.body

Postman Pre-request Script

Đặt các biến accessKey, secretKey, partnerCode, amount, redirectUrl, ipnUrl và bankAccounts trong Environment. Sau đó dùng script:

const CryptoJS = pm.require('npm:[email protected]');

const accessKey = pm.environment.get('accessKey');
const secretKey = pm.environment.get('secretKey');
const partnerCode = pm.environment.get('partnerCode');
const amount = String(pm.environment.get('amount'));
const redirectUrl = pm.environment.get('redirectUrl');
const ipnUrl = pm.environment.get('ipnUrl');
const bankAccounts = JSON.parse(pm.environment.get('bankAccounts'));
const requestId = Date.now().toString();
const orderId = `ORDER-${requestId}`;
const orderInfo = `TT${requestId}`;
const requestType = 'pay2s';

const metadata = {
  invoiceType: 'vat',
  customerInfo: {
    buyerContactName: 'NGUYỄN VĂN A',
    buyerCompanyName: 'CÔNG TY TNHH ABC',
    taxCode: '0312345678',
    citizenId: '',
    address: '123 Nguyễn Huệ, TP.HCM',
    email: '[email protected]',
    phone: '0901234567'
  },
  items: [{
    itemCode: 'DV001',
    itemName: 'Dịch vụ phần mềm',
    unit: 'Tháng',
    quantity: 1,
    unitPrice: 100000,
    taxRate: 10
  }],
  invoiceOptions: {
    requested: true,
    buyerNotTakingInvoice: false,
    paymentMethod: 'Chuyển khoản',
    note: '',
    source: 'api'
  }
};

const extraData = CryptoJS.enc.Base64.stringify(
  CryptoJS.enc.Utf8.parse(JSON.stringify(metadata))
);

const rawSignature =
  `accessKey=${accessKey}` +
  `&amount=${amount}` +
  `&bankAccounts=Array` +
  `&extraData=${extraData}` +
  `&ipnUrl=${ipnUrl}` +
  `&orderId=${orderId}` +
  `&orderInfo=${orderInfo}` +
  `&partnerCode=${partnerCode}` +
  `&redirectUrl=${redirectUrl}` +
  `&requestId=${requestId}` +
  `&requestType=${requestType}` +
  `&signatureVersion=2`;

const signature = CryptoJS
  .HmacSHA256(rawSignature, secretKey)
  .toString(CryptoJS.enc.Hex);

const payload = {
  accessKey,
  partnerCode,
  partnerName: 'CÔNG TY TNHH ABC',
  requestId,
  amount,
  orderId,
  orderInfo,
  orderType: requestType,
  bankAccounts,
  redirectUrl,
  ipnUrl,
  requestType,
  signatureVersion: '2',
  extraData,
  signature
};

pm.request.body.update(JSON.stringify(payload));

Body của Postman phải chọn raw → JSON. Không để các placeholder dạng hai dấu ngoặc nhọn ghi đè lại payload sau khi script chạy.

Tương thích với V1

Lỗi thường gặp

Thông báo Cách kiểm tra
Chữ ký không hợp lệ So sánh từng ký tự trong raw signature với payload thực tế
extraData không đúng Base64 JSON UTF-8 rồi Base64 đúng một lần
extraData không chứa JSON hợp lệ Giải mã Base64 và kiểm tra lại JSON
Thông tin hàng hóa không hợp lệ Kiểm tra tên, số lượng, đơn giá và thuế suất
Đơn ở mãi trong Hàng chờ Kiểm tra thanh toán, mẫu hóa đơn, tự động hóa và tổng tiền
Người mua thành “Bán cho người tiêu dùng” Bổ sung họ tên + địa chỉ, hoặc tên đơn vị + MST + địa chỉ
Đơn có MST không được tự động xử lý Kiểm tra trạng thái MST; chỉ mã trạng thái 00 được tiếp tục tự động

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.

Đã gọi API demo/UAT.

POST /v1/gateway/api/create

Body:

{
  "accessKey": "[REDACTED]",
  "partnerCode": "PAY2S7EPF0SB1ZP27W71",
  "partnerName": "Cửa hàng trải nghiệm",
  "requestId": "DEVd33eb13817916599723201625c7",
  "amount": 250000,
  "orderId": "DEVd33eb13817916599723201625c7",
  "orderInfo": "TT817916599723201625c7",
  "bankAccounts": [
    {
      "account_number": "P2S99999999",
      "bank_id": "ACB"
    }
  ],
  "redirectUrl": "https://developer.example.com/demos/?mode=live",
  "ipnUrl": "https://developer.example.com/sandbox/live-receiver/ipn",
  "requestType": "pay2s",
  "signature": "<SIGNATURE>",
  "signatureVersion": "2",
  "extraData": "eyJpbnZvaWNlVHlwZSI6InZhdCIsImN1c3RvbWVySW5mbyI6eyJidXllckNvbnRhY3ROYW1lIjoiQsOhbiBjaG8gbmfGsOG7nWkgdGnDqnUgZMO5bmcifSwiaXRlbXMiOlt7Iml0ZW1OYW1lIjoiU+G6o24gcGjhuqltIGRlbW8iLCJxdWFudGl0eSI6MSwidW5pdFByaWNlIjoyNTAwMDAsInRheFJhdGUiOjB9XSwiaW52b2ljZU9wdGlvbnMiOnsicmVxdWVzdGVkIjp0cnVlLCJidXllck5vdFRha2luZ0ludm9pY2UiOnRydWV9fQ=="
}

HTTP 200. Response:

{
  "partnerCode": "PAY2S7EPF0SB1ZP27W71",
  "requestId": "DEVd33eb13817916599723201625c7",
  "orderId": "DEVd33eb13817916599723201625c7",
  "orderInfo": "TT817916599723201625c7",
  "amount": 250000,
  "message": "Thành công",
  "lang": "vi",
  "resultCode": 0,
  "qrList": [
    {
      "bank_id": "ACB",
      "account_number": "P2S99999999",
      "account_name": "TAI KHOAN DEMO ACB",
      "qrCode": "<QRCODE_OMITTED>",
      "qrData": "00020101021238550010A000000727012500069704160111P2S999999990208QRIBFTTA52040000530370454062500005802VN62260822TT817916599723201625c7630495CF",
      "qrUrl": "https://sandbox-payment.pay2s.vn/quicklink/ACB/P2S99999999/TAI%20KHOAN%20DEMO%20ACB?amount=250000&memo=TT817916599723201625c7&is_mask=0"
    }
  ],
  "payUrl": "https://sandbox-payment.pay2s.vn/v2/gateway/pay?t=<REDACTED>",
  "storeId": null,
  "credentialScope": "owner",
  "isDemo": true,
  "environment": "demo"
}
Tìm trong 57 trang tài liệu · Esc để đóng