Appearance
Collection Link V2 – Tạo đơn có dữ liệu HĐĐT
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.
V2 không đổi endpoint
Endpoint vẫn là https://payment.pay2s.vn/v1/gateway/api/create. V2 được nhận diện bằng signatureVersion=2 và metadata Base64 trong extraData.
Luồng xử lý
- Website tạo Collection Link V2, kèm người mua và hàng hóa.
- Pay2S tự sinh
orderInfochuẩn, dùng mã này trong QR và trả về cùngpayUrl. - Khách hàng thanh toán.
- Khi thanh toán thành công, Pay2S gửi IPN đến
ipnUrlvà ghi nhận yêu cầu HĐĐT. - Đơ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:
- Có kết nối nhà cung cấp HĐĐT đang bật và hoạt động.
- Có mẫu hóa đơn mặc định, hoặc cửa hàng tương ứng đã được gán mẫu.
- Bật tự động hóa nếu muốn tự tạo nháp hoặc tự phát hành.
- Gửi
invoiceOptions.requested=truetrong đơn cần xuất hóa đơ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
- URL:
https://payment.pay2s.vn/v1/gateway/api/create - Method:
POST - Content-Type:
application/json; charset=UTF-8
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ụ:
json
[
{
"account_number": "99999999",
"bank_id": "ACB"
}
]Không gửi Secret Key trong Request
secretKey chỉ dùng tại backend của đối tác để tạo chữ ký. Tuyệt đối không đưa trường này vào payload hoặc mã JavaScript chạy trên trình duyệt.
Cấu trúc metadata trước khi Base64
json
{
"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:
- Cá nhân: có họ tên và địa chỉ.
- Doanh nghiệp: có tên đơn vị, mã số thuế và địa chỉ.
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
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:
- Chỉ trạng thái
00– NNT đang hoạt động, đã được cấp MST – được tiếp tục tự động. - Kết quả tra cứu chỉ bổ sung tên đơn vị và địa chỉ còn thiếu.
- Dữ liệu hợp lệ đã gửi trong
customerInfođược ưu tiên. - Nếu bật dùng tên người thanh toán, tên chuyển khoản chỉ được dùng cho
buyerContactName; không ghi đèbuyerCompanyName,taxCodehoặcaddress. - Kết quả tra cứu không được lưu cache.
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:
json
{
"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ụ |
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 |
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:
-2: không kê khai, tính nộp thuế.-1: không chịu thuế.
Kiểm tra tổng tiền
text
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:
- Hóa đơn GTGT: gửi
taxRateđúng thuế suất của từng dòng. Không cần gửivatReductionEligiblevàdirectVatRate. - Hóa đơn bán hàng không giảm theo Nghị quyết 204: thông thường gửi
taxRate=0và bỏ hai trường giảm thuế, hoặc gửivatReductionEligible=false. - Hóa đơn bán hàng có giảm theo Nghị quyết 204: gửi
taxRate=0,vatReductionEligible=truevàdirectVatRatebằng1,2,3hoặc5trên từng dòng đủ điều kiện.
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:
text
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ảmVí dụ một đơn có một món được giảm và một món không được giảm:
json
{
"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.
Khả năng hỗ trợ của nhà cung cấp
Pay2S chỉ phát hành dữ liệu giảm thuế trực tiếp khi API của nhà cung cấp đã được xác nhận hỗ trợ. Nếu nhà cung cấp chưa hỗ trợ, hóa đơn được giữ ở bản nháp và không gửi đi để tránh phát hành sai.
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ự:
- Chuyển metadata thành JSON UTF-8.
- Base64 chuỗi JSON đúng một lần.
- Dùng chính chuỗi Base64 đó trong payload và chuỗi ký.
Ví dụ Node.js:
js
const extraData = Buffer
.from(JSON.stringify(metadata), 'utf8')
.toString('base64');Ví dụ PHP:
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ý:
text
accessKey={accessKey}&amount={amount}&bankAccounts=Array&extraData={extraData}&ipnUrl={ipnUrl}&orderId={orderId}&orderInfo={orderInfo}&partnerCode={partnerCode}&redirectUrl={redirectUrl}&requestId={requestId}&requestType={requestType}&signatureVersion=2Tạo chữ ký bằng HMAC SHA-256 với secretKey, xuất kết quả dạng hex chữ thường.
Dữ liệu ký phải giống dữ liệu gửi
Chỉ cần khác một ký tự trong amount, extraData, URL hoặc orderInfo, API sẽ trả về Chữ ký không hợp lệ.
Request mẫu đầy đủ
json
{
"accessKey": "YOUR_ACCESS_KEY",
"partnerCode": "YOUR_PARTNER_CODE",
"partnerName": "CÔNG TY TNHH ABC",
"requestId": "1786680000000",
"amount": "110000",
"orderId": "ORDER-1786680000000",
"orderInfo": "TT1786680000000",
"orderType": "pay2s",
"bankAccounts": [
{
"account_number": "99999999",
"bank_id": "ACB"
}
],
"redirectUrl": "https://merchant.example/return",
"ipnUrl": "https://merchant.example/pay2s/ipn",
"requestType": "pay2s",
"signatureVersion": "2",
"extraData": "BASE64_OF_METADATA_JSON",
"signature": "HMAC_SHA256_SIGNATURE"
}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ụ:
text
P2S8A4F21C09D7BMã do Pay2S sinh:
- Chỉ gồm tiền tố
P2S, chữ in hoa và số. - Dài 15 ký tự, phù hợp nội dung chuyển khoản.
- Được kiểm tra không trùng trước khi sử dụng.
- Được dùng thống nhất trong QR, dữ liệu đơn hàng và IPN.
- Được tái sử dụng nếu đối tác gọi lại cùng
orderIdchưa thanh toán.
Response tạo đơn trả lại orderInfo thực tế:
json
{
"partnerCode": "YOUR_PARTNER_CODE",
"requestId": "1786680000000",
"orderId": "ORDER-1786680000000",
"orderInfo": "P2S8A4F21C09D7B",
"amount": "110000",
"message": "Thành công",
"resultCode": 0,
"qrList": [],
"payUrl": "https://payment.pay2s.vn/v2/gateway/pay?t=..."
}Giá trị request và response khác nhau
Khi tạo chữ ký, dùng đúng orderInfo đang gửi trong request. Khi hiển thị nội dung thanh toán hoặc lưu để đối soát, dùng orderInfo do response của Pay2S trả về. Hệ thống đối tác nên nhận diện đơn bằng orderId.
💻 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
<?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;js
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);
});csharp
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());python
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())java
// 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());
}
}go
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))
}ruby
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.bodyPostman Pre-request Script
Đặt các biến accessKey, secretKey, partnerCode, amount, redirectUrl, ipnUrl và bankAccounts trong Environment. Sau đó dùng script:
js
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
- Request V1 cũ không gửi
extraDatavà không gửisignatureVersionvẫn hoạt động. - Không thêm
extraDatavào chuỗi ký V1 của client cũ. - Khi gửi metadata hóa đơn, luôn chuyển sang chữ ký V2 và ký cả
extraData. - Có thể triển khai V2 dưới dạng tùy chọn bật/tắt để tránh ảnh hưởng khách đang dùng module cũ.
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 |
Bảo mật
Chỉ tạo chữ ký ở backend. Không đặt secretKey trong JavaScript chạy trên trình duyệt, ứng dụng public hoặc kho mã nguồn công khai.
