<!-- pay2s-offline-docs-v1 -->
# Tài liệu Pay2S đính kèm

Ngày chụp nội dung tài liệu: 2026-09-29 (UTC).
Nguồn: https://docs.pay2s.vn/

Đây là bản tài liệu lưu tại ngày trên, không tự cập nhật sau khi tải. Nội dung các trang được chép đầy đủ bên dưới; không cần mở link để đọc. Các liên kết khác là nguồn tham khảo bổ sung. Nếu thiếu thông tin cần thiết, hãy hỏi người dùng thay vì tự đoán.

## Mục lục
- 🏦 Danh sách Ngân hàng hỗ trợ
- API hủy đơn Collection Link
- SDK Collection Link V2 — Node.js/TypeScript và PHP
- Collection Link V2 – Tạo đơn có dữ liệu HĐĐT
- 🔗 Collection Link – Tạo liên kết thanh toán
- 🚀 Giới thiệu Pay2S API
- Instant Payment Notification (IPN)
- Xử lý kết quả thanh toán (Payment Notification)
- Mô phỏng thanh toán đơn demo
- 🧩 Tích hợp kỹ thuật
- Xác thực chữ ký (Signature)

---

Nguồn trang: https://docs.pay2s.vn/api/bankcode.html

# 🏦 Danh sách Ngân hàng hỗ trợ

## 📋 API lấy danh sách Ngân hàng

Để lấy danh sách các Ngân hàng hỗ trợ vui lòng sử dụng API dưới đây.

### 🔸 HTTP Request

<div class="code-block"><span class="http-method">POST</span> /v1/gateway/api/bankcodes </div>

### 🧾 Thông tin trả về
Thông tin trả ra theo từng Ngân hàng:


|Attribute|	Type	|Required	|Description|
|-------------|-------------|-------------|-------------|
|napasCode|	String|	✓|	The first napascode digits|
|disburseCode|	String|	✓|	The first disburseCode digits|
|name|	String|	✓|	Bank name|
|bankLogoUrl|	String|	✓|	Bank logo url|

### 📦 Mẫu Response
````json
{
    "VCB": {
        "napasCode": "970436",
        "disburseCode": "970436",
        "name": "VietcomBank",
        "bankLogoUrl": "https://img.pay2s.vn/v1/img/VCB.png"
    },
    "CTG": {
        "napasCode": "970415",
        "disburseCode": "970415",
        "name": "VietinBank",
        "bankLogoUrl": "https://img.pay2s.vn/v1/img/CTG.png"
    },
    "TCB": {
        "napasCode": "970407",
        "disburseCode": "970407",
        "name": "Techcombank",
        "bankLogoUrl": "https://img.pay2s.vn/v1/img/TCB.png"
    },
    "BIDV": {
        "napasCode": "970418",
        "disburseCode": "970418",
        "name": "BIDV",
        "bankLogoUrl": "https://img.pay2s.vn/v1/img/BIDV.png"
    },
    "ACB": {
        "napasCode": "970416",
        "disburseCode": "970416",
        "name": "ACB",
        "bankLogoUrl": "https://img.pay2s.vn/v1/img/ACB.png"
    
    },
    },
    ...
}
````

### Danh sách Ngân hàng và Đầu thẻ tương ứng

| Ngân hàng | STT | Mã ngân hàng | Logo | Tên Ngân hàng | Đầu thẻ | Hỗ trợ Chi hộ | Hỗ trợ Thanh toán |
| --- | --- | --- | --- | --- | --- | --- | --- |
| NH TMCP Ngoại Thương VN | 1 | VCB | ![VCB](https://img.mservice.com.vn/momo_app_v2/img/VCB.png) | VietcomBank | 970436 | ✓ | ✓ |
| NH TMCP Công Thương VN | 2 | CTG | ![CTG](https://img.mservice.com.vn/momo_app_v2/img/CTG.png) | VietinBank | 970415 | ✓ | ✓ |
| NH TMCP Kỹ Thương VN | 3 | TCB | ![TCB](https://img.mservice.com.vn/momo_app_v2/img/TCB.png) | Techcombank | 970407 | ✓ | ✓ |
| NH TMCP Đầu Tư và Phát Triển VN | 4 | BIDV | ![BIDV](https://img.mservice.com.vn/momo_app_v2/img/BIDV.png) | BIDV | 970418 | ✓ | ✓ |
| NH TMCP A Chau | 5 | ACB | ![ACB](https://img.mservice.com.vn/momo_app_v2/img/ACB.png) | ACB | 970416 | ✓ | ✓ |
| NH TMCP Quan Doi | 6 | MBB | ![MBB](https://img.mservice.com.vn/momo_app_v2/img/MB.png) | MBB | 970422 | ✓ | ✓ |
| NH TMCP Tien Phong | 7 | TPB | ![TPB](https://img.mservice.com.vn/momo_app_v2/img/TPB.png) | TPB | 970423 | ✓ | ✓ |

Bạn có thể tải logo các Ngân hàng tại đây.

<a href="/downloads/img/logo_bank.zip" class="custom-button">
  <span>&#x1F4E9;</span> Logo Thanh Toán
</a>

---

Nguồn trang: https://docs.pay2s.vn/api/cancel-order.html

# API hủy đơn Collection Link

`POST https://payment.pay2s.vn/v1/gateway/api/cancel`

API dành cho đối tác sử dụng accessKey/partnerCode/secretKey của API create.
API app `POST /api/v1/orders/cancel` dùng Bearer token giữ nguyên.

Áp dụng cho đơn tạo bằng [Collection Link V1](/api/collection-link) và
[Collection Link V2](/api/collection-link-v2). Chỉ hủy đơn chưa thanh toán.

## Request và chữ ký

Gửi JSON, tất cả trường là chuỗi:

```json
{
  "accessKey": "ACCESS_KEY",
  "partnerCode": "PARTNER_CODE",
  "orderId": "ORDER_123",
  "requestId": "CANCEL_123",
  "requestType": "cancel",
  "signature": "HMAC_SHA256_HEX"
}
```

- `orderId`: đúng orderId khách gửi lúc tạo đơn, không phải mã PAY2SJSC do Pay2S sinh.
- `requestId`: mã yêu cầu hủy do khách tạo, được trả lại để đối chiếu. Không phải requestId của lần create.
- `requestType`: bắt buộc `cancel`.
- Không gửi secretKey, extraData hoặc signatureVersion.
- Chuỗi ký nối đúng thứ tự dưới đây, không URL-encode, không thêm khoảng trắng:

```text
accessKey=ACCESS_KEY&orderId=ORDER_123&partnerCode=PARTNER_CODE&requestId=CANCEL_123&requestType=cancel
```

`signature = HMAC-SHA256(rawSignature, secretKey)` dạng hex.
Thực hiện ký từ backend; không đưa secretKey lên trình duyệt.

## Postman pre-request script

Đặt Environment: `accessKey`, `partnerCode`, `secretKey`, `orderId`.
Chọn Body → raw → JSON trước khi chạy:

```js
const CryptoJS = pm.require('npm:crypto-js@4.2.0');
const accessKey = pm.environment.get('accessKey');
const partnerCode = pm.environment.get('partnerCode');
const secretKey = pm.environment.get('secretKey');
const orderId = pm.environment.get('orderId');
if (![accessKey, partnerCode, secretKey, orderId].every(Boolean)) {
  throw new Error('Thiếu accessKey, partnerCode, secretKey hoặc orderId');
}
const requestId = 'CANCEL-' + Date.now();
const requestType = 'cancel';
const raw = `accessKey=${accessKey}&orderId=${orderId}&partnerCode=${partnerCode}&requestId=${requestId}&requestType=${requestType}`;
const signature = CryptoJS.HmacSHA256(raw, secretKey).toString(CryptoJS.enc.Hex);
pm.request.headers.upsert({key: 'Content-Type', value: 'application/json'});
pm.request.body.update(JSON.stringify({accessKey, partnerCode, orderId, requestId, requestType, signature}));
```

## Kết quả

HTTP 200 khi hủy thành công hoặc đơn đã hủy trước đó:

```json
{
  "partnerCode": "PARTNER_CODE",
  "requestId": "CANCEL_123",
  "orderId": "ORDER_123",
  "invoiceNumber": "PAY2SJSC...",
  "status": "cancelled",
  "resultCode": 0,
  "message": "Đơn hàng đã được hủy thành công."
}
```

Lỗi trả `{"status":false,"message":"..."}`:

| HTTP | Ý nghĩa |
| --- | --- |
| 400 | Trường dữ liệu hoặc requestType không hợp lệ |
| 401 | Thông tin xác thực hoặc chữ ký sai |
| 404 | Không có đơn trong phạm vi khóa đang dùng |
| 409 | Đơn không còn pending, hoặc orderId trùng nhiều đơn |
| 429 | Quá nhiều yêu cầu từ cùng IP |
| 500 | Lỗi xử lý; có thể gọi lại và kiểm tra trạng thái |

Chỉ cập nhật `pending → cancelled`, không xóa đơn hoặc hoàn tiền. Gọi lại đơn đã
hủy trả thành công; không ghi lại trạng thái. Khóa cửa hàng chỉ truy cập đơn của
cửa hàng đó; khóa chủ tài khoản truy cập đơn không gắn cửa hàng, giống phạm vi
create. Nếu legacy tạo nhiều đơn cùng orderId với nội dung khác nhau, trả 409 để
tránh chọn nhầm. Nên dùng orderId duy nhất cho mỗi đơn.

Hủy đơn trên Pay2S không vô hiệu hóa khả năng chuyển khoản tới tài khoản ngân hàng.
API này không gọi hủy QR phía ngân hàng, không phát webhook hủy, không hủy hóa đơn
điện tử. Khách tích hợp nhận kết quả trực tiếp từ response.


---

Nguồn trang: https://docs.pay2s.vn/api/collection-link-v2-sdk.html

# SDK Collection Link V2 — Node.js/TypeScript và PHP

SDK 0.1.0 đóng gói tạo thanh toán V2 và kiểm tra IPN. Dùng trên **backend của merchant**, không dùng ở trình duyệt/mobile vì cần Secret Key. Bản này được phân phối bằng file tải, **chưa publish lên npm hoặc Packagist**.

- <a href="/downloads/pay2s-collection-link-sdk-0.1.0.tgz" download>Tải SDK Node.js/TypeScript (.tgz)</a>
- [Tải SDK PHP (.zip)](/downloads/pay2s-collection-link-php-0.1.0.zip)

## 1. Cài đặt

### Node.js >=20

Tải file `.tgz` về dự án, sau đó:

```bash
npm install ./pay2s-collection-link-sdk-0.1.0.tgz
```

```js
const { Pay2S, Pay2SError } = require('@pay2s/collection-link-sdk');
```

TypeScript/ESM:

```ts
import { Pay2S, type PaymentInput } from '@pay2s/collection-link-sdk';
```

### PHP >=8.1

Cần extension `curl`, `json`, `mbstring`. Giải nén ZIP vào `packages/pay2s-collection-link/` trong dự án (file `composer.json` của SDK nằm ngay trong thư mục này). Thêm vào `composer.json` **của dự án**, giữ lại cấu hình có sẵn:

```json
{
  "repositories": [
    {"type":"path","url":"packages/pay2s-collection-link","options":{"symlink":false}}
  ]
}
```

Sau đó chạy:

```bash
composer require pay2s/collection-link-sdk:0.1.0
```

```php
require __DIR__ . '/vendor/autoload.php';
use Pay2S\CollectionLink\Pay2S;
```

Nếu chưa dùng Composer, có thể require trực tiếp `src/Pay2SError.php` và `src/Pay2S.php` trong thư mục SDK.

## 2. Cấu hình

Đặt ở môi trường backend, không commit giá trị thật:

```dotenv
PAY2S_PARTNER_CODE=
PAY2S_ACCESS_KEY=
PAY2S_SECRET_KEY=
```

```js
const pay2s = new Pay2S({
  partnerCode: process.env.PAY2S_PARTNER_CODE,
  accessKey: process.env.PAY2S_ACCESS_KEY,
  secretKey: process.env.PAY2S_SECRET_KEY,
  timeoutMs: 30000
});
```

Endpoint được cố định là `https://payment.pay2s.vn/v1/gateway/api/create`. SDK không tự tạo credential, kết nối ngân hàng hoặc cấu hình HĐĐT trên Pay2S.

## 3. Tạo thanh toán

Lưu đơn và `requestId` ở database trước khi gọi. Lấy tiền và sản phẩm từ dữ liệu server, không tin tổng tiền do trình duyệt gửi lên.

```js
const result = await pay2s.createPayment({
  orderId: 'ORDER_20260918_001',
  requestId: 'REQUEST_20260918_001',
  orderInfo: 'TT ORDER_20260918_001',
  amount: 110000,
  bankAccounts: [{ account_number: 'YOUR_ACCOUNT_NUMBER', bank_id: 'ACB' }],
  redirectUrl: 'https://shop.example/payment/return',
  ipnUrl: 'https://shop.example/api/pay2s/ipn',
  metadata: {
    invoiceType: 'vat',
    customerInfo: { buyerContactName: 'Bán cho người tiêu dùng' },
    items: [{ itemName: 'Sản phẩm mẫu', quantity: 1, unitPrice: 100000, taxRate: 10 }],
    invoiceOptions: { requested: true, buyerNotTakingInvoice: true, source: 'api' }
  }
});

// Lưu result.orderInfo (mã P2S... thực tế), result.payUrl vào đơn ở database.
// Sau khi lưu xong mới trả result.payUrl cho trình duyệt để khách thanh toán.
```

`metadata` là object JSON, **chưa Base64**. SDK tạo `extraData`, cố định `signatureVersion: "2"`, ký HMAC đúng thứ tự và gửi request. Dùng `itemName`, không dùng `productName`. Toàn bộ metadata người mua/hàng hóa xem [Collection Link V2](/api/collection-link-v2).

SDK yêu cầu `orderId`, `requestId`, `orderInfo` do website cung cấp; không tự sinh để tránh đổi mã khi gọi lại. `amount` là số hoặc chuỗi thập phân dương, tối đa 10 tỷ VND, tối đa 2 chữ số thập phân. Số tài khoản là chuỗi. `quantity`, `unitPrice`, `taxRate` là số JSON. Metadata tối đa 50 dòng, Base64 tối đa 32 KiB.

SDK kiểm tra định dạng cơ bản nhưng **không tự tính tổng hoặc suy đoán loại mẫu/thuế**. Merchant phải gửi tổng thanh toán khớp dữ liệu đơn và hàng hóa, đặc biệt khi có giảm thuế trực tiếp.

### PHP tương đương

```php
$pay2s = new Pay2S([
    'partnerCode' => getenv('PAY2S_PARTNER_CODE'),
    'accessKey' => getenv('PAY2S_ACCESS_KEY'),
    'secretKey' => getenv('PAY2S_SECRET_KEY'),
]);
$result = $pay2s->createPayment([
    'orderId' => 'ORDER_20260918_001',
    'requestId' => 'REQUEST_20260918_001',
    'orderInfo' => 'TT ORDER_20260918_001',
    'amount' => 110000,
    'bankAccounts' => [['account_number' => 'YOUR_ACCOUNT_NUMBER', 'bank_id' => 'ACB']],
    'redirectUrl' => 'https://shop.example/payment/return',
    'ipnUrl' => 'https://shop.example/api/pay2s/ipn',
    'metadata' => [
        'invoiceType' => 'vat',
        'customerInfo' => ['buyerContactName' => 'Bán cho người tiêu dùng'],
        'items' => [['itemName' => 'Sản phẩm mẫu', 'quantity' => 1, 'unitPrice' => 100000, 'taxRate' => 10]],
        'invoiceOptions' => ['requested' => true, 'buyerNotTakingInvoice' => true, 'source' => 'api'],
    ],
]);
// Lưu $result['orderInfo'], $result['payUrl'] vào đơn trước khi trả URL cho khách.
```

Muốn tự dùng HTTP client sẵn có: gọi `buildPayment(input)` để lấy payload đã ký, rồi POST JSON **nguyên payload** tới endpoint. Không sửa trường đã ký và không ký lại theo công thức V1.

## 4. Nhận IPN và xử lý đơn

`verifyIpn(data)` trả boolean, chỉ kiểm tra chữ ký `m2signature` và danh tính bộ khóa. `assertPaidIpn(data, expected)` kiểm tra thêm `resultCode = 0`, mã đơn, số tiền; kiểm tra `orderInfo` nếu truyền giá trị đã lưu từ response.

```js
if (!pay2s.verifyIpn(req.body)) {
  return res.status(401).json({ success: false });
}

// Tìm đơn trong database theo req.body.orderId, giới hạn đúng tài khoản merchant.
// expected phải lấy từ database, KHÔNG copy số tiền từ chính req.body.
pay2s.assertPaidIpn(req.body, {
  orderId: storedOrder.orderId,
  amount: storedOrder.amount,
  orderInfo: storedOrder.pay2sOrderInfo
});
```

PHP:

```php
$ipn = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($ipn) || !$pay2s->verifyIpn($ipn)) {
    http_response_code(401);
    header('Content-Type: application/json');
    echo json_encode(['success' => false]);
    exit;
}
// $storedOrder lấy từ database và khóa trong transaction ở bước xử lý bên dưới.
$pay2s->assertPaidIpn($ipn, [
    'orderId' => $storedOrder['order_id'],
    'amount' => $storedOrder['amount'],
    'orderInfo' => $storedOrder['pay2s_order_info'],
]);
```

Các đoạn trên minh họa gọi SDK; `req/res`, `storedOrder` và transaction thuộc ứng dụng của bạn. Khi triển khai endpoint IPN, phải có đầy đủ luồng:

1. Parse JSON có giới hạn dung lượng; dữ liệu sai trả 400, chữ ký sai trả 401.
2. Xác thực chữ ký trước khi xử lý dữ liệu thanh toán.
3. Bắt đầu transaction, đọc và **khóa đơn** theo `orderId` đúng tài khoản merchant.
4. Đối chiếu số tiền và `orderInfo` đã lưu bằng `assertPaidIpn`.
5. Nếu đơn đã paid: không cộng tiền/giao hàng lần hai. Nếu chưa: cập nhật trạng thái paid cùng mã giao dịch và ghi tác vụ giao hàng vào outbox trong cùng transaction.
6. Commit rồi trả **HTTP 200, `{"success":true}`** cho cả lần đầu và IPN hợp lệ gửi lại. Nếu database lỗi, rollback và trả lỗi để Pay2S có thể gửi lại.

Nếu IPN có chữ ký hợp lệ nhưng `resultCode` khác 0, `assertPaidIpn` ném lỗi `NOT_PAID`; xử lý như thông báo không thành công, tuyệt đối không cộng tiền. Xử lý exception trong handler của bạn, không để lỗi SDK làm treo request.

SDK **không tự lưu trạng thái hoặc chống replay bằng database**. Hai IPN giống nhau đều có chữ ký hợp lệ. Việc khóa đơn và cập nhật có điều kiện là bắt buộc để tránh cộng tiền hai lần.

`requestId` trong IPN có thể là ID nội bộ Pay2S, không bắt buộc bằng requestId lúc tạo đơn. `requestTrace` có thể xuất hiện nhưng không nằm trong chuỗi ký, không dùng nó để xác nhận thanh toán. Luôn nhận diện đơn bằng `orderId` đã ký và đối chiếu tiền từ database.

Không dùng thông tin redirect trình duyệt để đánh dấu paid. Chữ ký redirect khác chữ ký IPN và sẽ không qua `verifyIpn`.

## 5. Lỗi và gọi lại

Node.js dùng `error.code`; PHP dùng `$error->errorCode`. Cả hai có `httpStatus` và `resultCode` khi có phản hồi API.

| Mã SDK | Xử lý |
|---|---|
| `INVALID_INPUT` | Sửa dữ liệu trước khi gọi; request chưa được gửi |
| `TRANSPORT_ERROR` | Mạng/timeout; kết quả có thể chưa rõ, giữ orderId và đối soát |
| `INVALID_RESPONSE` | Phản hồi không đúng JSON/cấu trúc; không kết luận đơn chưa tạo |
| `API_ERROR` | Kiểm tra HTTP/resultCode; có thể là từ chối, rate limit hoặc lỗi hệ thống |
| `INVALID_IPN` | Không xử lý thanh toán |
| `NOT_PAID` | Không xác nhận thành công, không giao hàng/cộng tiền |
| `ORDER_MISMATCH` | Đối soát mã đơn/số tiền/nội dung; không đánh dấu paid |

Không tự retry trong SDK. Nếu cần thử lại sau lỗi mạng, kiểm tra đơn/IPN trước và giữ mã đơn gốc; không tạo orderId mới chỉ vì timeout. Không đánh dấu thanh toán thất bại vĩnh viễn chỉ vì request tạo link bị timeout.

## 6. Kiểm thử và hướng dẫn cho AI tích hợp

Gói SDK có test fixture dùng khóa giả và transport mô phỏng. Node: `node --test test.cjs`; PHP: `php tests.php` trong thư mục gói. Test không gọi API thật.

Khi giao tài liệu cho AI coding assistant, yêu cầu:

> Dùng SDK Collection Link V2 của Pay2S để tích hợp vào backend hiện tại. Đọc tài liệu SDK, Collection Link V2 và IPN. Tái sử dụng model đơn hàng, quản lý bí mật, transaction và queue sẵn có. Tạo endpoint thanh toán cùng endpoint IPN có kiểm tra chữ ký, đối chiếu dữ liệu từ database, khóa đơn và chống xử lý trùng. Không xác nhận thanh toán từ redirect. Không tự đổi orderId khi timeout. Viết test cho chữ ký sai, IPN gửi lại/đồng thời, sai tiền và database lỗi. Liệt kê cấu hình cần cung cấp, migration và file đã sửa.

Đây là SDK **thanh toán kèm metadata HĐĐT**. Chưa bao gồm API HĐĐT độc lập hoặc tra cứu MST. HĐĐT chỉ vào hàng chờ/tự động sau khi thanh toán theo cấu hình Pay2S; không tự gọi thêm API hóa đơn độc lập cho cùng đơn để tránh xuất trùng.


---

Nguồn trang: https://docs.pay2s.vn/api/collection-link-v2.html

# 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.

Đã có [SDK Node.js/TypeScript và PHP](/api/collection-link-v2-sdk) để đó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.

::: info 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ý

Với tài khoản và ngân hàng demo, có thể dùng [API mô phỏng thanh toán](/api/simulate-payment) để 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/cancel-order). 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:

- 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=true` trong đơ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"
  }
]
```

::: warning 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": "ketoan@abc.vn",
    "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

Website có thể gọi [API tra cứu mã số thuế](/hoa-don-dien-tu/api-tra-cuu-ma-so-thue) 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:

- 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`, `taxCode` hoặc `address`.
- 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](/hoa-don-dien-tu/tra-cuu-ma-so-thue).

### 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ụ; 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:

- `-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ửi `vatReductionEligible` và `directVatRate`.
- **Hóa đơn bán hàng không giảm theo Nghị quyết 204:** thông thường gửi `taxRate=0` và bỏ hai trường giảm thuế, hoặc gửi `vatReductionEligible=false`.
- **Hóa đơn bán hàng có giảm theo Nghị quyết 204:** gửi `taxRate=0`, `vatReductionEligible=true` và `directVatRate` bằng `1`, `2`, `3` hoặc `5` trê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ảm
```

Ví 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.

::: warning 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ự:

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:

```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=2
```

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

::: danger 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
P2S8A4F21C09D7B
```

Mã 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 `orderId` chư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=..."
}
```

::: warning 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.

::: code-group

```php [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' => 'ketoan@abc.vn',
        '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 [Node.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: 'ketoan@abc.vn',
    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 [ASP/.NET]
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 = "ketoan@abc.vn",
        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 [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": "ketoan@abc.vn",
        "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 [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", "ketoan@abc.vn",
                "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 [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": "ketoan@abc.vn",
			"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 [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: 'ketoan@abc.vn',
    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:

```js
const CryptoJS = pm.require('npm:crypto-js@4.2.0');

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: 'ketoan@abc.vn',
    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 `extraData` và không gửi `signatureVersion` vẫn hoạt động.
- Không thêm `extraData` và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 |

::: warning 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.
:::


---

Nguồn trang: https://docs.pay2s.vn/api/collection-link.html

# 🔗 Collection Link – Tạo liên kết thanh toán

[Xem API hủy đơn hàng chưa thanh toán](/api/cancel-order).

Để kiểm thử tự động với đơn demo, dùng [API mô phỏng thanh toán](/api/simulate-payment) sau khi create thành công.

::: info Cần gửi dữ liệu xuất hóa đơn điện tử?
Tích hợp mới nên xem [Collection Link V2](/api/collection-link-v2). V2 bổ sung metadata người mua, hàng hóa và yêu cầu xuất HĐĐT nhưng vẫn giữ nguyên endpoint tạo đơn.
:::

## 🧠 Khái niệm

Collection Link là API dùng để khởi tạo đơn hàng từ website của merchant đến hệ thống Pay2S.

## 🔄 Luồng xử lý

1. Khách hàng thực hiện mua hàng trên Website hoặc ứng dụng của merchant và lựa chọn thanh toán trực tuyến Napas 247 cho đơn hàng.
2. Website hoặc ứng dụng của merchant tiến hành gọi tạo link thanh toán, Pay2S sẽ kiểm tra dữ liệu và trả về kết quả chứa link thanh toán. Khi hệ thống của merchant nhận kết quả link thanh toán cần chuyển hướng khách hàng của bạn đến trang checkout của Pay2S bằng cách mở link thanh toán từ kết quả.
3. Khách hàng sử dụng ứng dụng ngân hàng để quét mã VietQR từ link thanh toán.
4. Giao dịch ghi nhận thành công tại ngân hàng, Pay2S sẽ trả kết quả thành công về redirectUrl gồm: trạng thái, mã đơn hàng, mã link thanh toán, ... Từ kết quả nhận được trên redirectUrl Website hoặc ứng dụng của merchant hiển thị giao diện thành công.
5. Sau khi có kết quả ở giao diện, đồng thời Pay2S sẽ gửi một kết quả với đầy đủ thông tin thanh toán tới ipnUrl của cửa hàng, sau đó merchant cập nhật trạng thái đơn hàng phù hợp.

## ⚙️ Cấu hình API

### 🔸 HTTP Request

- **URL**: `https://payment.pay2s.vn/v1/gateway/api/create`
- **Method**: `POST`
- **Content-Type**: `application/json; charset=UTF-8`

### Request Parameters

| Key           | Type       | Required | Description                                        |
| ------------- | ---------- | -------- | -------------------------------------------------- |
| partnerCode   | String     | ✓        | Định danh duy nhất cho mỗi yêu cầu.                |
| partnerName   | String     |          | Tên đối tác.                                       |
| requestId     | String(50) | ✓        | Định danh duy nhất cho mỗi yêu cầu.                |
| amount        | Long       | ✓        | Số tiền cần thanh toán (VND).                      |
| bankAccounts | String     | ✓        | Mảng chứa các số tài khoản và Bank Code.  |
| orderId       | String     | ✓        | Mã đơn hàng của đối tác.                           |
| orderInfo     | String     | ✓        | Thông tin đơn hàng. Lưu ý: Giới hạn 10-32 ký tự, chỉ chấp nhậ ký tự chữ + số, không dấu gạch ngang hoặc đặc biêt.                             |
| redirectUrl   | String     | ✓        | URL chuyển hướng sau khi thanh toán.               |
| ipnUrl        | String     | ✓        | API nhận kết quả thanh toán của đối tác.           |
| requestType   | String     | ✓        | Định danh kiểu của request.                        |
| signature     | String     | ✓        | [Chữ ký](/others/signature.md) xác nhận giao dịch. Sử dụng thuật toán Hmac_SHA256 với data theo định dạng: 1 chuỗi String được sort theo thứ tự:` accessKey=$accessKey&amount=$amount&bankAccounts=Array&ipnUrl=$ipnUrl&orderId=$orderId&orderInfo=$orderInfo&partnerCode=$partnerCode&redirectUrl=$redirectUrl&requestId=$requestId&requestType=$requestType`                         |

### 📦 Sample Request

```json
{
  "accessKey": "YOUR_ACCESS_KEY",
  "partnerCode": "PAY2S7EPF0SB1ZP27W71",
  "partnerName": "Test Payment Woocommerce",
  "requestId": "1751916562",
  "amount": 2000,
  "orderId": "1173",
  "orderInfo": "TT1173",
  "orderType": "pay2s",
  "bankAccounts": [
    {
      "account_number": "99999999",
      "bank_id": "ACB"
    }
  ],
  "redirectUrl": "https://demo-payment.pay2s.vn/wp/thanh-toan/order-received/1173/?key=wc_order_qrJlIwVAwIEfl",
  "ipnUrl": "https://demo-payment.pay2s.vn/wp/wc-api/WC_Gateway_Pay2S/",
  "requestType": "pay2s",
  "signature": "40cb7802b7d16bd08b1fc8d2a9f08d9df5efd785c7e7a6e817d8c7c4f71f165e"
}
```

### 📦 Sample Response

```json
{
    "partnerCode": "PAY2S7EPF0SB1ZP27W71",
    "requestId": "1769253352840",
    "orderId": "1769253352840",
    "amount": "10000",
    "message": "Thành công",
    "lang": "vi",
    "resultCode": 0,
    "qrList": [
        {
            "bank_id": "ACB",
            "account_number": "99999999",
            "account_name": "PAY2S SANDBOX",
            "qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAeAAAAHgCAYAAAB91L6VAAAAAklEQVR4AewaftIAAA7rSURBVO3BwZEcuoIDMFLl/FPmvgy+Dm31zhhA958AAE+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM/9yQ/SNtzZln9Z23zattxqm2/alk9rm1vbcqNtbm3Lrbb5CbblVttwZ1t+ghMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4Lk/+aW25bdpm09rm2/alr9hW260zTdty79sW261DXe25bdpm9/mBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuT/5xbfNN2/JN2/Ibtc2NbfmN2ubGtnxT2/wN2/JNbfMTtM03bcu/7AQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO5P4FLb3NqWG23zTW3zTW1za1tubcunbcuttvkJ2ubWttzaFvhfTgCA504AgOdOAIDnTgCA504AgOdOAIDnTgCA504AgOdOAIDnTgCA5/6Ef1rb3NqWW21zY1tutc2tbfm0trm1LZ/WNt+0Lbe25Ubb3NqWT9uWW23zadvCv+sEAHjuBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO5P/nHb8i/blm9qm1vbcqttPm1bbrXNp23LN7XNrW25sS1/Q9t807b8BNvC95wAAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM/9yS/VNvxvbXNrWz5tW261za1tudE237Qtt9rm1rbcaJtb23KrbW5sy622ubUtN9rm1rbcapsb2/I3tA3//50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM91/wn8Mm1zY1v+hrb5pm250Ta3tuXT2uZv2JYbbfM3bAv8LycAwHMnAMBzJwDAcycAwHMnAMBzJwDAcycAwHMnAMBzJwDAc91/8kO0za1tudE2t7blVtvc2JZbbXNrW76pbfjftuVW29zalm9qm99mW36btrm1LZ/WNre25bc5AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCe6/6TX6htfoJt+Rva5sa23GqbT9uWv6Ft+N+25Zva5ta23GqbG9tyq21ubcuNtvmmbbnVNre25Ubb/A3b8hOcAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADP/ckP0jbftC232ubT2gZubMuntc2tbbmxLX/Dttxom1vbcqttbmzLrbb5bbblVtv8NicAwHMnAMBzJwDAcycAwHMnAMBzJwDAcycAwHMnAMBzJwDAc3/yj9uWn2Jbfopt+aZtudE2t7blVtvc2Ja/oW1ubMtP0Tafti232ubWtnzattxqm29qmxvb8i87AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCe+5MfZFtutc03bcuNtvmmbfmmtrm1Lbfa5sa23Gqbb2qbW9tyo23+ZW1za1tutc2NbbnVNp/WNre25Vbb8L+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADP/ckvtS032uan2JZbbfNp23KrbW5sy622+bS2ubUt3GmbG9vyU7TNrW250Ta3tuWn2JYbbXNrW36bEwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5P/ml2ubGttxqm29qm09rm99oW76pbT5tWz5tW36jbbnRNre25Vbb/ARtc2tbbrXNp7XNrW35CU4AgOdOAIDnTgCA504AgOdOAIDnTgCA504AgOdOAIDnTgCA504AgOf+5B/XNn/DtnxT23zatnxa29zallttc2NbbrXNrW35prb5pm250Ta3tuVW23xa29zalhtt8ze0zY1tudU2t7blRtvc2pbf5gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO5PfqltudE2t7bl09rmm7blb2ibb9qWG21za1v+Zdtyq21ubMuttrm1LTfa5ta23GqbT2ubW9tyo21ubcuttrmxLbfa5ta2/AQnAMBzJwDAcycAwHMnAMBzJwDAcycAwHMnAMBzJwDAcycAwHPdf8LXtM2tbeFO23zTttxom1vbcqttPm1bPq1tvmlbfqO2ubEtf0PbfNq2/DYnAMBzJwDAcycAwHMnAMBzJwDAcycAwHMnAMBzJwDAcycAwHMnAMBz3X/yQ7QNn7Utt9rm07blb2ibT9uWb2qbn2JbfoK2ubUtt9rmxrbcapufYls+rW1ubctPcAIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPPcnv9S28L+1zd+wLTfa5ta23NqWT2ubW9vyTdtyo23+hra5sS232ubWttzYllttc2tbbrTNrW251TY3tuVvaJtP25bf5gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47k/4K9rmt9mWW23zTW1zY1tubcuttrmxLX9D23zTttxom59iW261zae1za1t+bS2ubUtn9Y2t7blJzgBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ7r/pMfom0+bVu+qW1ubQt32uabtuVG29zalm9qm2/allttc2NbbrXNrW250Ta3tuVW29zYlltt81Nsy09wAgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA89ye/1LbcaJu/YVtubMuttvkptuXT2ubTtuVvaJsb23Krbb5pWz6tbW61za1t+bRtudU2N7blVtt807Z8Wtv8y04AgOdOAIDnTgCA504AgOdOAIDnTgCA504AgOdOAIDnTgCA57r/5Bdqmxvbcqttbm3LN7XNjW35G9rmxrb8DW1zY1u+qW1+o235pra5sS232ubTtuVvaJsb23KrbW5ty6e1za1t+QlOAIDnTgCA504AgOdOAIDnTgCA504AgOdOAIDnTgCA504AgOdOAIDn/oRr2/JpbfNTtM1PsS3f1DY3tuVW29zalhtt8ze0zY1tudU2t7blt2mbT2ubb2qbW9vy25wAAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM/9CV+1LX9D29xom29qm2/alltt81O0zY1t+Rva5kbbcGdbbrXNjW251TZ81gkA8NwJAPDcCQDw3AkA8NwJAPDcCQDw3AkA8NwJAPDcCQDw3J/8IG1za1tutM2tbbnVNje25VbbfNO2/DZt8xtty422ubUtn7Yt39Q2v9G2fNq23GqbG9tyq21ubctPcAIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPNf9J79Q29zYllttc2tbbrTNrW35Kdrm07blVtt82rbcaptP25ZbbXNjW76pbb5pW/6GtvkJtuVW29zaFv63EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDgue4/+SHa5ta23GibW9vyaW3zTdtyq21ubcuntc2tbfm0trm1LTfa5m/Ylhttc2tbvqltbm3Ljba5tS232ua32ZZbbfNN2/ITnAAAz50AAM+dAADPnQAAz50AAM+dAADPnQAAz50AAM+dAADPdf8J/6y2+aZt+Rva5sa2/BRt803bcqttPm1bPq1tbm3Lrba5sS3f1Da3toXPOgEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnvuTH6RtuLMtN7blb2ibb9qWG21za1tutc2nbcuntc03bcuttvmXtc2tbfm0tvkptuUnOAEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnvuTX2pbfpu2+aa2ubUtN9rm1rbcaptPa5tP25a/oW1ubMuttrm1LTfa5qdom2/alp9iW260zb/sBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuBAB47gQAeO4EAHjuT/5xbfNN2/JNbXNrW/5l2/JTbMunbcs3bcuntc03tc1v1DY3tuVfdgIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPPcn/NO25W9omxvb8jdsy422udU2t7blm9rmxrbcaptb23JjW261zadty622ubUtN9rm1rZ8U9vc2pYbbXNrW36bEwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguT/hn9Y2t7bl1rb8NtvyTW1za1tutM2tbbnVNje25W/Ylhttc2tbbrXNp7XNrW250TZ/Q9vc2JZbbXNrW36CEwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5P/nHbcu/bFtutc1PsS2f1ja3tuVG2/wNbXNjW261za1t+aa2+bS2ubUtN9rm1rbcapsb23KrbW5ty6dty29zAgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA8dwIAPHcCADx3AgA81/0nP0TbcGdbbrTNrW3hTtvc2JZbbXNrW260zb9sW/6GtrmxLbfa5ta2/DZtc2tbfoITAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC5EwDguRMA4LkTAOC57j8BAJ46AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnjsBAJ47AQCeOwEAnvs/pOUe2+omoYIAAAAASUVORK5CYII=",
            "qrUrl": "https://sandbox-payment.pay2s.vn/quicklink/ACB/99999999/PAY2S SANDBOX?amount=10000&memo=DH1769253352840&is_mask=0"
        }
    ],
    "payUrl": "https://sandbox-payment.pay2s.vn/v2/gateway/pay?t=UEFZMlNKU0M2OTc0QTlFQUUzRThFfDE3NjkyNTM5NTQuOTMzNXx7ImJhbmtMaXN0IjpbeyJhY2NvdW50X251bWJlciI6Ijk5OTk5OTk5IiwiYmFua19pZCI6IkFDQiJ9XX0%3D"
}
```

## 💻 Code mẫu

::: code-group
``` php [PHP]

function execPostRequest($url, $data)
{
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json',
        'Content-Length: ' . strlen($data))
    );
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    
    $result = curl_exec($ch);
    curl_close($ch);
    
    echo($result);
    $jsonResult = json_decode($result, true);
    
    // Điều hướng đến URL thanh toán nếu có
    if (isset($jsonResult['payUrl'])) {
        header('Location: ' . $jsonResult['payUrl']);
    }
    
    return $result;
}

$endpoint = 'https://payment.pay2s.vn/v1/gateway/api/create';
$merchantName = "TEST";
$accessKey = '';
$secretKey = '';
$orderInfo = 'pay with Pay2S';
$order_desc = 'DH{{orderid}}';
$partnerCode = 'PAY2S72MLKFJFURCGPEM';
$redirectUrl = 'https://webhook.site/dd3de633-0a13-40e8-996d-ccd195d7237b';
$ipnUrl = 'https://webhook.site/dd3de633-0a13-40e8-996d-ccd195d7237b';
$amount = '50000';
$bank_accounts = "9877644888|vcb";
$orderId = time()."";
$requestId = time()."";
$extraData = '';
$requestType = 'pay2s';
$partnerName = 'Pay2S Payment';
$lang = 'vi';

if (!empty($_POST)) {
    $accessKey = $_POST["accessKey"];
    $secretKey = $_POST["secretKey"];
    $orderInfo = $_POST["orderInfo"];
    $partnerCode = $_POST["partnerCode"];
    $redirectUrl = $_POST["redirectUrl"];
    $amount = $_POST["amount"];
    $orderId = $_POST["orderId"];
    $order_desc = $_POST['order_desc'];
    $orderInfo = str_replace('{{orderid}}', $orderId, $_POST['order_desc']);
    $bank_accounts = $_POST['bank_accounts'];
    
    $lines = explode("\n", $bank_accounts);
    $bankList = [];
    foreach ($lines as $line) {
        // Tách từng dòng thành account_number và bank_id
        list($accountNumber, $bankId) = explode('|', trim($line));
        // Đưa các giá trị này vào mảng kết quả
        $bankList[] = [
            'account_number' => trim($accountNumber),
            'bank_id' => trim($bankId)
        ];
    }

    $requestId = time() . '';
    $extraData = "";
   
    // Tạo chữ ký HMAC SHA256
    $rawHash = "accessKey="YOUR_ACCESS_KEY"&amount=" . $amount . "&bankAccounts=Array&ipnUrl=" . $ipnUrl . "&orderId=" . $orderId . "&orderInfo=" . $orderInfo . "&partnerCode=" . $partnerCode . "&redirectUrl=" . $redirectUrl . "&requestId=" . $requestId . "&requestType=" . $requestType;
    $signature = hash_hmac("sha256", $rawHash, $secretKey);

    $data = array(
        'accessKey' => $accessKey,
        'partnerCode' => $partnerCode,
        'partnerName' => $merchantName,
        'requestId' => $requestId,
        'amount' => $amount,
        'orderId' => $orderId,
        'orderInfo' => $orderInfo,
        'orderType' => $requestType,
        'bankAccounts' => $bankList,
        'redirectUrl' => $redirectUrl,
        'ipnUrl' => $ipnUrl,
        'requestType' => $requestType,
        'signature' => $signature,
    );

    $result = execPostRequest($endpoint, json_encode($data));
    $jsonResult = json_decode($result, true);  // decode json
}
?>
```
```js [NodeJS]
const axios = require('axios');
const crypto = require('crypto');

// Các giá trị mặc định
let endpoint = 'https://payment.pay2s.vn/v1/gateway/api/create';
let merchantName = "TEST";
let accessKey = '';
let secretKey = '';
let orderInfo = 'pay with Pay2S';
let order_desc = 'DH{{orderid}}';
let partnerCode = 'PAY2S72MLKFJFURCGPEM';
let redirectUrl = 'https://webhook.site/dd3de633-0a13-40e8-996d-ccd195d7237b';
let ipnUrl = 'https://webhook.site/dd3de633-0a13-40e8-996d-ccd195d7237b';
let amount = '50000';
let bank_accounts = "9877644888|vcb";
let orderId = Date.now() + '';
let requestId = Date.now() + '';
let extraData = '';
let requestType = 'pay2s';
let partnerName = 'Pay2S Payment';
let lang = 'vi';

// Dữ liệu POST
accessKey = 'YOUR_ACCESS_KEY'; // Thay thế bằng giá trị từ form POST
secretKey = 'YOUR_SECRET_KEY'; // Thay thế bằng giá trị từ form POST
orderInfo = 'pay with Pay2S';
partnerCode = 'yourPartnerCode'; // Thay thế bằng giá trị từ form POST
redirectUrl = 'https://your-redirect-url.com';
amount = '50000'; // Thay thế bằng giá trị từ form POST
orderId = Date.now() + '';
order_desc = order_desc.replace('{{orderid}}', orderId);
bank_accounts = '9877644888|vcb'; // Thay thế bằng giá trị từ form POST

// Chuyển đổi danh sách tài khoản ngân hàng
const lines = bank_accounts.split('\n');
const bankList = lines.map(line => {
    const [accountNumber, bankId] = line.trim().split('|');
    return {
        account_number: accountNumber.trim(),
        bank_id: bankId.trim(),
    };
});

// Tạo chữ ký HMAC SHA256
const rawHash = `accessKey=${accessKey}&amount=${amount}&bankAccounts=Array&ipnUrl=${ipnUrl}&orderId=${orderId}&orderInfo=${orderInfo}&partnerCode=${partnerCode}&redirectUrl=${redirectUrl}&requestId=${requestId}&requestType=${requestType}`;
const signature = crypto.createHmac('sha256', secretKey).update(rawHash).digest('hex');

// Dữ liệu gửi đi
const data = {
    accessKey: accessKey,
    partnerCode: partnerCode,
    partnerName: merchantName,
    requestId: requestId,
    amount: amount,
    orderId: orderId,
    orderInfo: orderInfo,
    orderType: requestType,
    bankAccounts: bankList,
    redirectUrl: redirectUrl,
    ipnUrl: ipnUrl,
    requestType: requestType,
    signature: signature,
};

// Hàm thực hiện POST request
async function execPostRequest(url, data) {
    try {
        const response = await axios.post(url, data, {
            headers: {
                'Content-Type': 'application/json',
            },
        });
        return response.data;
    } catch (error) {
        console.error('Error:', error.response ? error.response.data : error.message);
    }
}

// Gửi yêu cầu tới API
execPostRequest(endpoint, data).then(result => {
    console.log('Result:', result);
});
```
```js [ASP]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace Pay2SExample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class Pay2SController : ControllerBase
    {
        private readonly HttpClient _httpClient;

        public Pay2SController(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        [HttpPost("create-payment")]
        public async Task<IActionResult> CreatePayment()
        {
            string endpoint = "https://payment.pay2s.vn/v1/gateway/api/create";
            string merchantName = "TEST";
            string accessKey = "YOUR_ACCESS_KEY";
            string secretKey = "YOUR_SECRET_KEY";
            string orderInfo = "pay with Pay2S";
            string orderDesc = "DH{{orderid}}";
            string partnerCode = "PAY2S72MLKFJFURCGPEM";
            string redirectUrl = "https://your-redirect-url.com";
            string ipnUrl = "https://your-ipn-url.com";
            string amount = "50000";
            string bankAccounts = "9877644888|vcb";
            string orderId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
            string requestId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
            string requestType = "pay2s";
            string partnerName = "Pay2S Payment";

            // Tạo danh sách tài khoản ngân hàng
            var bankList = bankAccounts.Split("\n")
                .Select(line =>
                {
                    var parts = line.Split('|');
                    return new
                    {
                        account_number = parts[0].Trim(),
                        bank_id = parts[1].Trim()
                    };
                }).ToList();

            // Tạo chuỗi chữ ký (rawHash)
            string rawHash = $"accessKey={accessKey}&amount={amount}&bankAccounts=Array&ipnUrl={ipnUrl}&orderId={orderId}&orderInfo={orderInfo}&partnerCode={partnerCode}&redirectUrl={redirectUrl}&requestId={requestId}&requestType={requestType}";

            // Tạo chữ ký HMAC SHA256
            string signature = CreateSignature(rawHash, secretKey);

            // Chuẩn bị dữ liệu POST
            var requestData = new
            {
                accessKey,
                partnerCode,
                partnerName = merchantName,
                requestId,
                amount,
                orderId,
                orderInfo,
                orderType = requestType,
                bankAccounts = bankList,
                redirectUrl,
                ipnUrl,
                requestType,
                signature
            };

            // Gửi yêu cầu POST tới API
            var response = await _httpClient.PostAsync(endpoint, new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json"));

            // Xử lý phản hồi
            string result = await response.Content.ReadAsStringAsync();
            return Ok(JsonConvert.DeserializeObject(result));
        }

        // Hàm tạo chữ ký HMAC SHA256
        private string CreateSignature(string rawData, string secretKey)
        {
            var key = Encoding.UTF8.GetBytes(secretKey);
            using (var hmac = new HMACSHA256(key))
            {
                var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawData));
                return BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }
    }
}
```
```python [Python]
import requests
import hmac
import hashlib
import json
import time

def create_signature(raw_data, secret_key):
    """Tạo chữ ký HMAC SHA256"""
    signature = hmac.new(
        secret_key.encode('utf-8'),
        raw_data.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return signature

def create_payment():
    endpoint = 'https://payment.pay2s.vn/v1/gateway/api/create'
    merchant_name = "TEST"
    access_key = "YOUR_ACCESS_KEY"
    secret_key = "YOUR_SECRET_KEY"
    order_info = "pay with Pay2S"
    partner_code = "PAY2S72MLKFJFURCGPEM"
    redirect_url = "https://your-redirect-url.com"
    ipn_url = "https://your-ipn-url.com"
    amount = "50000"
    bank_accounts = "9877644888|vcb"
    order_id = str(int(time.time()))
    request_id = str(int(time.time()))
    request_type = "pay2s"

    # Tạo danh sách tài khoản ngân hàng
    bank_list = []
    for line in bank_accounts.split('\n'):
        parts = line.strip().split('|')
        bank_list.append({
            'account_number': parts[0].strip(),
            'bank_id': parts[1].strip()
        })

    # Tạo chuỗi chữ ký
    raw_hash = f"accessKey={access_key}&amount={amount}&bankAccounts=Array&ipnUrl={ipn_url}&orderId={order_id}&orderInfo={order_info}&partnerCode={partner_code}&redirectUrl={redirect_url}&requestId={request_id}&requestType={request_type}"
    
    # Tạo chữ ký
    signature = create_signature(raw_hash, secret_key)

    # Chuẩn bị dữ liệu
    data = {
        'accessKey': access_key,
        'partnerCode': partner_code,
        'partnerName': merchant_name,
        'requestId': request_id,
        'amount': amount,
        'orderId': order_id,
        'orderInfo': order_info,
        'orderType': request_type,
        'bankAccounts': bank_list,
        'redirectUrl': redirect_url,
        'ipnUrl': ipn_url,
        'requestType': request_type,
        'signature': signature,
    }

    # Gửi yêu cầu
    response = requests.post(endpoint, json=data, headers={'Content-Type': 'application/json'})
    
    if response.status_code == 200:
        result = response.json()
        print(json.dumps(result, indent=2))
        return result
    else:
        print(f"Error: {response.status_code}")
        return None

if __name__ == "__main__":
    create_payment()
```
```java [Java]
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;

public class Pay2SPayment {
    
    public static String createSignature(String rawData, String secretKey) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8),
            0,
            secretKey.getBytes(StandardCharsets.UTF_8).length,
            "HmacSHA256"
        );
        mac.init(secretKeySpec);
        byte[] hash = mac.doFinal(rawData.getBytes(StandardCharsets.UTF_8));
        
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    public static void main(String[] args) throws Exception {
        String endpoint = "https://payment.pay2s.vn/v1/gateway/api/create";
        String merchantName = "TEST";
        String accessKey = "YOUR_ACCESS_KEY";
        String secretKey = "YOUR_SECRET_KEY";
        String orderInfo = "pay with Pay2S";
        String partnerCode = "PAY2S72MLKFJFURCGPEM";
        String redirectUrl = "https://your-redirect-url.com";
        String ipnUrl = "https://your-ipn-url.com";
        String amount = "50000";
        String bankAccounts = "9877644888|vcb";
        String orderId = String.valueOf(System.currentTimeMillis());
        String requestId = String.valueOf(System.currentTimeMillis());
        String requestType = "pay2s";

        // Tạo danh sách tài khoản ngân hàng
        JsonArray bankList = new JsonArray();
        for (String line : bankAccounts.split("\\n")) {
            String[] parts = line.trim().split("\\|");
            JsonObject bank = new JsonObject();
            bank.addProperty("account_number", parts[0].trim());
            bank.addProperty("bank_id", parts[1].trim());
            bankList.add(bank);
        }

        // Tạo chuỗi chữ ký
        String rawHash = String.format(
            "accessKey=%s&amount=%s&bankAccounts=Array&ipnUrl=%s&orderId=%s&orderInfo=%s&partnerCode=%s&redirectUrl=%s&requestId=%s&requestType=%s",
            accessKey, amount, ipnUrl, orderId, orderInfo, partnerCode, redirectUrl, requestId, requestType
        );

        String signature = createSignature(rawHash, secretKey);

        // Chuẩn bị dữ liệu
        JsonObject data = new JsonObject();
        data.addProperty("accessKey", accessKey);
        data.addProperty("partnerCode", partnerCode);
        data.addProperty("partnerName", merchantName);
        data.addProperty("requestId", requestId);
        data.addProperty("amount", amount);
        data.addProperty("orderId", orderId);
        data.addProperty("orderInfo", orderInfo);
        data.addProperty("orderType", requestType);
        data.add("bankAccounts", bankList);
        data.addProperty("redirectUrl", redirectUrl);
        data.addProperty("ipnUrl", ipnUrl);
        data.addProperty("requestType", requestType);
        data.addProperty("signature", signature);

        // Gửi yêu cầu
        URL url = new URL(endpoint);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setDoOutput(true);

        String jsonData = new Gson().toJson(data);
        try (OutputStream os = conn.getOutputStream()) {
            os.write(jsonData.getBytes(StandardCharsets.UTF_8));
        }

        int responseCode = conn.getResponseCode();
        System.out.println("Response Code: " + responseCode);
    }
}
```
```go [Go]
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"
)

type BankAccount struct {
	AccountNumber string `json:"account_number"`
	BankID        string `json:"bank_id"`
}

type PaymentRequest struct {
	AccessKey     string         `json:"accessKey"`
	PartnerCode   string         `json:"partnerCode"`
	PartnerName   string         `json:"partnerName"`
	RequestID     string         `json:"requestId"`
	Amount        string         `json:"amount"`
	OrderID       string         `json:"orderId"`
	OrderInfo     string         `json:"orderInfo"`
	OrderType     string         `json:"orderType"`
	BankAccounts  []BankAccount  `json:"bankAccounts"`
	RedirectURL   string         `json:"redirectUrl"`
	IPNURL        string         `json:"ipnUrl"`
	RequestType   string         `json:"requestType"`
	Signature     string         `json:"signature"`
}

func createSignature(rawData, secretKey string) string {
	h := hmac.New(sha256.New, []byte(secretKey))
	h.Write([]byte(rawData))
	return fmt.Sprintf("%x", h.Sum(nil))
}

func main() {
	endpoint := "https://payment.pay2s.vn/v1/gateway/api/create"
	merchantName := "TEST"
	accessKey := "yourAccessKey"
	secretKey := "yourSecretKey"
	orderInfo := "pay with Pay2S"
	partnerCode := "PAY2S72MLKFJFURCGPEM"
	redirectURL := "https://your-redirect-url.com"
	ipnURL := "https://your-ipn-url.com"
	amount := "50000"
	bankAccounts := "9877644888|vcb"
	orderID := fmt.Sprintf("%d", time.Now().UnixMilli())
	requestID := fmt.Sprintf("%d", time.Now().UnixMilli())
	requestType := "pay2s"

	// Tạo danh sách tài khoản ngân hàng
	var bankList []BankAccount
	for _, line := range strings.Split(bankAccounts, "\n") {
		parts := strings.Split(strings.TrimSpace(line), "|")
		bankList = append(bankList, BankAccount{
			AccountNumber: strings.TrimSpace(parts[0]),
			BankID:        strings.TrimSpace(parts[1]),
		})
	}

	// Tạo chuỗi chữ ký
	rawHash := fmt.Sprintf(
		"accessKey=%s&amount=%s&bankAccounts=Array&ipnUrl=%s&orderId=%s&orderInfo=%s&partnerCode=%s&redirectUrl=%s&requestId=%s&requestType=%s",
		accessKey, amount, ipnURL, orderID, orderInfo, partnerCode, redirectURL, requestID, requestType,
	)

	signature := createSignature(rawHash, secretKey)

	// Chuẩn bị dữ liệu
	payment := PaymentRequest{
		AccessKey:    accessKey,
		PartnerCode:  partnerCode,
		PartnerName:  merchantName,
		RequestID:    requestID,
		Amount:       amount,
		OrderID:      orderID,
		OrderInfo:    orderInfo,
		OrderType:    requestType,
		BankAccounts: bankList,
		RedirectURL:  redirectURL,
		IPNURL:       ipnURL,
		RequestType:  requestType,
		Signature:    signature,
	}

	// Gửi yêu cầu
	jsonData, _ := json.Marshal(payment)
	response, err := http.Post(endpoint, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer response.Body.Close()

	body, _ := ioutil.ReadAll(response.Body)
	fmt.Println(string(body))
}
```
```ruby [Ruby]
require 'net/http'
require 'json'
require 'openssl'
require 'time'

def create_signature(raw_data, secret_key)
  OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_data)
end

endpoint = 'https://payment.pay2s.vn/v1/gateway/api/create'
merchant_name = 'TEST'
access_key = 'YOUR_ACCESS_KEY'
secret_key = 'YOUR_SECRET_KEY'
order_info = 'pay with Pay2S'
partner_code = 'PAY2S72MLKFJFURCGPEM'
redirect_url = 'https://your-redirect-url.com'
ipn_url = 'https://your-ipn-url.com'
amount = '50000'
bank_accounts = '9877644888|vcb'
order_id = (Time.now.to_f * 1000).to_i.to_s
request_id = (Time.now.to_f * 1000).to_i.to_s
request_type = 'pay2s'

# Tạo danh sách tài khoản ngân hàng
bank_list = bank_accounts.split("\n").map do |line|
  parts = line.strip.split('|')
  {
    account_number: parts[0].strip,
    bank_id: parts[1].strip
  }
end

# Tạo chuỗi chữ ký
raw_hash = "accessKey=#{access_key}&amount=#{amount}&bankAccounts=Array&ipnUrl=#{ipn_url}&orderId=#{order_id}&orderInfo=#{order_info}&partnerCode=#{partner_code}&redirectUrl=#{redirect_url}&requestId=#{request_id}&requestType=#{request_type}"

signature = create_signature(raw_hash, secret_key)

# Chuẩn bị dữ liệu
data = {
  accessKey: access_key,
  partnerCode: partner_code,
  partnerName: merchant_name,
  requestId: request_id,
  amount: amount,
  orderId: order_id,
  orderInfo: order_info,
  orderType: request_type,
  bankAccounts: bank_list,
  redirectUrl: redirect_url,
  ipnUrl: ipn_url,
  requestType: request_type,
  signature: signature
}

# Gửi yêu cầu
uri = URI(endpoint)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
request.body = data.to_json

response = http.request(request)
puts response.body
```
:::



---

Nguồn trang: https://docs.pay2s.vn/api/gioi-thieu.html

# 🚀 Giới thiệu Pay2S API

Chào mừng bạn đến với **Pay2S API** – giải pháp thanh toán trực tuyến toàn diện và hiệu quả cho mọi nhu cầu giao dịch của bạn.

Với **Pay2S API**, chúng tôi cung cấp cho bạn một công cụ **mạnh mẽ**, **dễ dàng tích hợp**, và **an toàn tuyệt đối** để khởi tạo, xử lý và xác nhận giao dịch một cách nhanh chóng.

---

## 🎯 Lợi ích khi sử dụng Pay2S API

- ⚡ **Nhanh chóng và tiện lợi**  
  Tích hợp dễ dàng vào hệ thống của bạn, giúp khởi động giao dịch trực tuyến mà không gặp trở ngại.

- 🔒 **An toàn và bảo mật**  
  Đáp ứng các tiêu chuẩn bảo mật cao nhất (HTTPS, Token, HMAC...), đảm bảo mọi giao dịch luôn được bảo vệ.

- 🏦 **Hỗ trợ đa dạng ngân hàng & phương thức thanh toán**  
  Tích hợp đa ngân hàng và phương thức chuyển khoản, mã QR, ví điện tử...

- 🔧 **Tùy biến linh hoạt**  
  Dễ dàng cấu hình theo yêu cầu hệ thống, mô hình kinh doanh và trải nghiệm người dùng.

---

Hãy khám phá và trải nghiệm những tiện ích mà **Pay2S API** mang lại, và biến mọi giao dịch trực tuyến của bạn trở nên đơn giản và hiệu quả hơn bao giờ hết.

👉 Xem tiếp: [Tài liệu tích hợp kỹ thuật API](/api/tich-hop-ky-thuat.html)


---

Nguồn trang: https://docs.pay2s.vn/api/instant-payment-notification.html

# Instant Payment Notification (IPN)

## Giới thiệu

**Instant Payment Notification (IPN)** là cơ chế **thông báo giao dịch tức thì** từ hệ thống Pay2S đến máy chủ của merchant ngay khi có thay đổi trạng thái thanh toán, ví dụ như: giao dịch thành công, thất bại hoặc bị hủy.

Thông báo IPN được gửi tự động dưới dạng HTTP POST đến một địa chỉ URL do merchant cấu hình trong hệ thống Pay2S.

---

## 📌 Mục đích của IPN

- Giúp merchant **nhận biết tức thời** trạng thái giao dịch.
- Cho phép merchant **xử lý tự động** các hành động sau thanh toán như kích hoạt dịch vụ, gửi hàng, cập nhật trạng thái đơn hàng, v.v.
- Đảm bảo **tính tự động, chính xác, và nhanh chóng** trong quy trình vận hành.

---

## ⚙️ Cơ chế hoạt động

1. Merchant tạo đơn hàng thông qua API Pay2S.
2. Người dùng tiến hành thanh toán.
3. Khi hệ thống Pay2S xác nhận giao dịch, một yêu cầu HTTP POST sẽ được gửi đến IPN URL mà merchant đã cấu hình.
4. Thông báo IPN chứa dữ liệu về giao dịch: `transactionId`, `orderId`, `amount`, `status`, `signature`, ...
5. Merchant cần xác thực chữ ký (`signature`) để đảm bảo dữ liệu không bị giả mạo.
6. Merchant phản hồi HTTP status code `200 OK` để xác nhận đã nhận thành công.

---


| Attribute | Value                                     | Description                  |
| --------- | ----------------------------------------- | ---------------------------- |
| URL       | ipnUrl                                    | URL                          |
| Method    | <span class="http-method">POST</span>     | Phương thức gửi IPN |
| Headers   | Content-type: application/json            | HTTP Headers                 |
| Payload   | Nội dung JSON giao dịch | Thông tin giao dịch được gửi từ Pay2S |

**Mẫu Request IPN**

```json
curl -X 'POST' 'https://example.com/pay2s_ipn' -H 'content-type: application/json' -d $'{
  "partnerCode":"PAY2S",
  "orderId":"01234567890123451633504872421",
  "requestId":"01234567890123451633504872421",
  "amount":1000,
  "orderInfo":"Test Thue 1234556",
  "orderType":"Pay2S_wallet",
  "transId":2588659987,
  "resultCode":0,
  "message":"Giao dịch thành công.",
  "payType":"qr",
  "m2signature":"90482b3881bdf863d5f61ace078921bbc6dbb58b2fded35261c71c9af3b1ce4f"
}'
```

Response của đối tác sẽ đến Pay2S sau khi Pay2S gửi HTTP request đến địa chỉ `ipnUrl`.

:::: info IMPORTANT
Đối tác cần phản hồi lại trong vòng **30 giây** với:

- HTTP status code: `200 OK`  
- Body: `{ "success": true }` (dưới dạng JSON)

Nếu không đúng cấu trúc trên, Pay2S sẽ tự động gửi lại IPN tối đa 5 lần.
:::: 

:::: info INFO
Đối tác cần kiểm tra tính hợp lệ của chữ ký trong IPN để đảm bảo kết quả của giao dịch. `Amount` và `Currency` trong notification phải khớp với `Amount` và `Currency` đối tác lưu trong database bên đó!
::::
Field resultCode và message do đối tác xử lý. Tham chiếu đến result code mà Pay2S trả về. Pay2S sẽ dùng những thông tin này để phản hồi lại khách hàng nếu có bất cứ lỗi nào xảy ra trong quá trình xử lý thanh toán bên nhà đối tác.

**Trạng Thái Giao Dịch** - Sử dụng field resultCode để xác định trạng thái của giao dịch:

- `resultCode = 0`: giao dịch thành công.
- `resultCode = 9000`: giao dịch được cấp quyền (authorization) thành công .
- `resultCode <> 0`: giao dịch thất bại. Tham khảo Result code để xác định chi tiết lỗi của giao dịch.

### 📘 Một số `resultCode` phổ biến

| resultCode | Mô tả                             |
|------------|-----------------------------------|
| 0          | Giao dịch thành công              |
| 9000       | giao dịch được cấp quyền (authorization) thành công.    |
| 2       | Giao dịch đã hủy hoặc không thành công            |
| 1002       | Sai chữ ký xác thực               |
| 1003       | Giao dịch không tồn tại           |

## 💻 Code mẫu

::: code-group

```php [PHP]
<?php
header("content-type: application/json; charset=UTF-8");
http_response_code(200); //200 - Everything will be 200 Oke
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
if (is_null($data)) {
    echo 'Invalid JSON data.';
    exit;
}
function response($data, $code = 200)
{
    http_response_code($code);
    header('Content-Type: application/json');
    echo json_encode($data);
    die();
}
$accessKey = "";
$secretKey = "";
$response = array();
try {
    $amount = $data['amount'];
    $extraData = isset($data['extraData']) ? $data['extraData'] : '';
    $message = $data['message'];
    $orderId = $data['orderId'];
    $orderInfo = $data['orderInfo'];
    $orderType = $data['orderType'];
    $partnerCode = $data['partnerCode'];
    $payType = $data['payType'];
    $requestId = $data['requestId'];
    $responseTime = $data['responseTime'];
    $resultCode = $data['resultCode'];
    $transId = $data['transId'];
    $m2signature = $data['m2signature'];


    //Checksum
    $rawHash = "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";

    $partnerSignature = hash_hmac("sha256", $rawHash, $secretKey);


    if ($m2signature == $partnerSignature) {
        if ($resultCode == '0') {
            $result = '<div class="alert alert-success">Capture Payment Success</div>';
        } else {
            $result = '<div class="alert alert-danger">' . $message . '</div>';
        }
    } else {
        $result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
    }
} catch (Exception $e) {
    echo $response['message'] = $e;
}

$debugger = array();
$debugger['rawData'] = $rawHash;
$debugger['pay2sSignature'] = $m2signature;
$debugger['partnerSignature'] = $partnerSignature;

if ($m2signature == $partnerSignature) {
    $response['success'] = true;
    $response['message'] = "Received payment result success";
} else {
    $response['success'] = false;
    $response['message'] = "ERROR! Fail checksum";
}
$response['debugger'] = $debugger;
echo json_encode($response);
```
```js [NodeJS]
const express = require("express");
const crypto = require("crypto");
const bodyParser = require("body-parser");

const app = express();
app.use(bodyParser.json());

app.post("/ipn", (req, res) => {
  const data = req.body;

  // Kiểm tra JSON hợp lệ
  if (!data) {
    return res
      .status(400)
      .json({ success: false, message: "Invalid JSON data." });
  }

  let accessKey = "";
  let secretKey = "";

  try {
    // Lấy dữ liệu từ request body
    const amount = data.amount;
    const extraData = data.extraData || "";
    const message = data.message;
    const orderId = data.orderId;
    const orderInfo = data.orderInfo;
    const orderType = data.orderType;
    const partnerCode = data.partnerCode;
    const payType = data.payType;
    const requestId = data.requestId;
    const responseTime = data.responseTime;
    const resultCode = data.resultCode;
    const transId = data.transId;
    const m2signature = data.m2signature;

    // Tạo chuỗi `rawHash` cho chữ ký
    const rawHash = `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}`;

    // Tạo chữ ký HMAC SHA256
    const partnerSignature = crypto
      .createHmac("sha256", secretKey)
      .update(rawHash)
      .digest("hex");

    // Kiểm tra chữ ký
    if (m2signature === partnerSignature) {
      if (resultCode === "0") {
        result =
          '<div class="alert alert-success">Capture Payment Success</div>';
      } else {
        result = `<div class="alert alert-danger">${message}</div>`;
      }
    } else {
      result =
        '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
    }

    // Debugger thông tin
    const debuggerInfo = {
      rawData: rawHash,
      pay2sSignature: m2signature,
      partnerSignature: partnerSignature,
    };

    // Trả về kết quả
    if (m2signature === partnerSignature) {
      return res.status(200).json({
        success: true,
        message: "Received payment result success",
        debugger: debuggerInfo,
      });
    } else {
      return res.status(200).json({
        success: false,
        message: "ERROR! Fail checksum",
        debugger: debuggerInfo,
      });
    }
  } catch (error) {
    return res.status(500).json({ success: false, message: error.message });
  }
});

// Khởi động server
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});
```
```js [ASP]
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace Pay2SIPN.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class Pay2SController : ControllerBase
    {
        [HttpPost("ipn")]
        public IActionResult IPN([FromBody] Pay2SRequest data)
        {
            // Kiểm tra JSON hợp lệ
            if (data == null)
            {
                return BadRequest(new { success = false, message = "Invalid JSON data." });
            }

            string accessKey = ""; // Khóa truy cập của bạn
            string secretKey = ""; // Khóa bí mật của bạn
            string partnerSignature;

            try
            {
                // Lấy các giá trị từ request body
                var amount = data.amount;
                var extraData = data.extraData ?? string.Empty;
                var message = data.message;
                var orderId = data.orderId;
                var orderInfo = data.orderInfo;
                var orderType = data.orderType;
                var partnerCode = data.partnerCode;
                var payType = data.payType;
                var requestId = data.requestId;
                var responseTime = data.responseTime;
                var resultCode = data.resultCode;
                var transId = data.transId;
                var m2signature = data.m2signature;

                // Tạo chuỗi rawHash
                var rawHash = $"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}";

                // Tạo chữ ký HMAC SHA256
                partnerSignature = CreateHmacSha256Signature(rawHash, secretKey);

                // Kiểm tra chữ ký
                if (m2signature == partnerSignature)
                {
                    if (resultCode == "0")
                    {
                        return Ok(new
                        {
                            success = true,
                            message = "Received payment result success",
                            debugger = new
                            {
                                rawData = rawHash,
                                pay2sSignature = signature,
                                partnerSignature = partnerSignature
                            }
                        });
                    }
                    else
                    {
                        return Ok(new
                        {
                            success = false,
                            message = message,
                            debugger = new
                            {
                                rawData = rawHash,
                                pay2sSignature = signature,
                                partnerSignature = partnerSignature
                            }
                        });
                    }
                }
                else
                {
                    return Ok(new
                    {
                        success = false,
                        message = "This transaction could be hacked, please check your signature and returned signature",
                        debugger = new
                        {
                            rawData = rawHash,
                            pay2sSignature = signature,
                            partnerSignature = partnerSignature
                        }
                    });
                }
            }
            catch (Exception ex)
            {
                return StatusCode(500, new { success = false, message = ex.Message });
            }
        }

        // Hàm tạo chữ ký HMAC SHA256
        private string CreateHmacSha256Signature(string data, string secretKey)
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
            {
                byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
                return BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }
    }

    // Lớp đại diện cho dữ liệu yêu cầu từ Pay2S
    public class Pay2SRequest
    {
        public string amount { get; set; }
        public string extraData { get; set; }
        public string message { get; set; }
        public string orderId { get; set; }
        public string orderInfo { get; set; }
        public string orderType { get; set; }
        public string partnerCode { get; set; }
        public string payType { get; set; }
        public string requestId { get; set; }
        public string responseTime { get; set; }
        public string resultCode { get; set; }
        public string transId { get; set; }
        public string m2signature { get; set; }
    }
}
```
```python [Python]
from flask import Flask, request, jsonify
import hmac
import hashlib
import json

app = Flask(__name__)

def verify_signature(data, signature, secret_key):
    """Xác thực chữ ký HMAC SHA256"""
    raw_hash = f"accessKey={data.get('accessKey')}&amount={data.get('amount')}&extraData={data.get('extraData', '')}&message={data.get('message')}&orderId={data.get('orderId')}&orderInfo={data.get('orderInfo')}&orderType={data.get('orderType')}&partnerCode={data.get('partnerCode')}&payType={data.get('payType')}&requestId={data.get('requestId')}&responseTime={data.get('responseTime')}&resultCode={data.get('resultCode')}&transId={data.get('transId')}"
    
    computed_signature = hmac.new(
        secret_key.encode('utf-8'),
        raw_hash.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return computed_signature == signature

@app.route('/ipn', methods=['POST'])
def handle_ipn():
    try:
        data = request.get_json()
        
        if not data:
            return jsonify({'success': False, 'message': 'Invalid JSON data'}), 400
        
        access_key = ""  # Khóa truy cập
        secret_key = ""  # Khóa bí mật
        
        m2signature = data.get('m2signature')
        result_code = data.get('resultCode')
        message = data.get('message')
        
        # Xác thực chữ ký
        if verify_signature(data, m2signature, secret_key):
            if result_code == '0':
                return jsonify({
                    'success': True,
                    'message': 'Received payment result success'
                }), 200
            else:
                return jsonify({
                    'success': False,
                    'message': message
                }), 200
        else:
            return jsonify({
                'success': False,
                'message': 'This transaction could be hacked, please check your signature'
            }), 200
            
    except Exception as e:
        return jsonify({'success': False, 'message': str(e)}), 500

if __name__ == '__main__':
    app.run(port=3000, debug=True)
```
```java [Java]
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

@RestController
@RequestMapping("/api")
public class Pay2SIPNController {
    
    private static String createSignature(String rawData, String secretKey) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8),
            0,
            secretKey.getBytes(StandardCharsets.UTF_8).length,
            "HmacSHA256"
        );
        mac.init(secretKeySpec);
        byte[] hash = mac.doFinal(rawData.getBytes(StandardCharsets.UTF_8));
        
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }
    
    @PostMapping("/ipn")
    public ResponseEntity<?> handleIPN(@RequestBody JsonObject data) {
        try {
            if (data == null) {
                return ResponseEntity.badRequest()
                    .body(new Gson().toJson(new IResponseData(false, "Invalid JSON data")));
            }
            
            String accessKey = ""; // Khóa truy cập
            String secretKey = ""; // Khóa bí mật
            
            // Lấy dữ liệu từ request
            String amount = data.get("amount").getAsString();
            String extraData = data.has("extraData") ? data.get("extraData").getAsString() : "";
            String message = data.get("message").getAsString();
            String orderId = data.get("orderId").getAsString();
            String orderInfo = data.get("orderInfo").getAsString();
            String orderType = data.get("orderType").getAsString();
            String partnerCode = data.get("partnerCode").getAsString();
            String payType = data.get("payType").getAsString();
            String requestId = data.get("requestId").getAsString();
            String responseTime = data.get("responseTime").getAsString();
            String resultCode = data.get("resultCode").getAsString();
            String transId = data.get("transId").getAsString();
            String m2signature = data.get("m2signature").getAsString();
            
            // Tạo chuỗi rawHash
            String rawHash = String.format(
                "accessKey=%s&amount=%s&extraData=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s&transId=%s",
                accessKey, amount, extraData, message, orderId, orderInfo, orderType, 
                partnerCode, payType, requestId, responseTime, resultCode, transId
            );
            
            // Tạo chữ ký
            String partnerSignature = createSignature(rawHash, secretKey);
            
            // Kiểm tra chữ ký
            if (m2signature.equals(partnerSignature)) {
                if ("0".equals(resultCode)) {
                    return ResponseEntity.ok(
                        new Gson().toJson(new IResponseData(true, "Received payment result success"))
                    );
                } else {
                    return ResponseEntity.ok(
                        new Gson().toJson(new IResponseData(false, message))
                    );
                }
            } else {
                return ResponseEntity.ok(
                    new Gson().toJson(new IResponseData(false, "This transaction could be hacked"))
                );
            }
            
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new Gson().toJson(new IResponseData(false, e.getMessage())));
        }
    }
    
    static class IResponseData {
        boolean success;
        String message;
        
        IResponseData(boolean success, String message) {
            this.success = success;
            this.message = message;
        }
    }
}
```
```go [Go]
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

type IPNData struct {
	Amount       string `json:"amount"`
	ExtraData    string `json:"extraData"`
	Message      string `json:"message"`
	OrderID      string `json:"orderId"`
	OrderInfo    string `json:"orderInfo"`
	OrderType    string `json:"orderType"`
	PartnerCode  string `json:"partnerCode"`
	PayType      string `json:"payType"`
	RequestID    string `json:"requestId"`
	ResponseTime string `json:"responseTime"`
	ResultCode   string `json:"resultCode"`
	TransID      string `json:"transId"`
	M2Signature  string `json:"m2signature"`
}

type ResponseData struct {
	Success bool   `json:"success"`
	Message string `json:"message"`
}

func createSignature(rawData, secretKey string) string {
	h := hmac.New(sha256.New, []byte(secretKey))
	h.Write([]byte(rawData))
	return fmt.Sprintf("%x", h.Sum(nil))
}

func handleIPN(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Error reading body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()
	
	var data IPNData
	err = json.Unmarshal(body, &data)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(ResponseData{Success: false, Message: "Invalid JSON data"})
		return
	}
	
	accessKey := "" // Khóa truy cập
	secretKey := "" // Khóa bí mật
	
	// Tạo chuỗi rawHash
	rawHash := fmt.Sprintf(
		"accessKey=%s&amount=%s&extraData=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s&transId=%s",
		accessKey, data.Amount, data.ExtraData, data.Message, data.OrderID, data.OrderInfo,
		data.OrderType, data.PartnerCode, data.PayType, data.RequestID, data.ResponseTime,
		data.ResultCode, data.TransID,
	)
	
	// Tạo chữ ký
	partnerSignature := createSignature(rawHash, secretKey)
	
	w.Header().Set("Content-Type", "application/json")
	
	// Kiểm tra chữ ký
	if data.M2Signature == partnerSignature {
		if data.ResultCode == "0" {
			json.NewEncoder(w).Encode(ResponseData{Success: true, Message: "Received payment result success"})
		} else {
			json.NewEncoder(w).Encode(ResponseData{Success: false, Message: data.Message})
		}
	} else {
		json.NewEncoder(w).Encode(ResponseData{Success: false, Message: "This transaction could be hacked"})
	}
}

func main() {
	http.HandleFunc("/ipn", handleIPN)
	log.Println("Server running on port 3000")
	log.Fatal(http.ListenAndServe(":3000", nil))
}
```
```ruby [Ruby]
require 'sinatra'
require 'json'
require 'openssl'

def verify_signature(data, signature, secret_key)
  raw_hash = "accessKey=#{data['accessKey']}&amount=#{data['amount']}&extraData=#{data['extraData'] || ''}&message=#{data['message']}&orderId=#{data['orderId']}&orderInfo=#{data['orderInfo']}&orderType=#{data['orderType']}&partnerCode=#{data['partnerCode']}&payType=#{data['payType']}&requestId=#{data['requestId']}&responseTime=#{data['responseTime']}&resultCode=#{data['resultCode']}&transId=#{data['transId']}"
  
  computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_hash)
  computed_signature == signature
end

post '/ipn' do
  content_type :json
  
  begin
    data = JSON.parse(request.body.read)
    
    if data.nil?
      return { success: false, message: 'Invalid JSON data' }.to_json
    end
    
    access_key = ''  # Khóa truy cập
    secret_key = ''  # Khóa bí mật
    
    signature = data['m2signature']
    result_code = data['resultCode']
    message = data['message']
    
    # Xác thực chữ ký
    if verify_signature(data, signature, secret_key)
      if result_code == '0'
        { success: true, message: 'Received payment result success' }.to_json
      else
        { success: false, message: message }.to_json
      end
    else
      { success: false, message: 'This transaction could be hacked, please check your signature' }.to_json
    end
    
  rescue => e
    { success: false, message: e.message }.to_json
  end
end
```
:::

## 🔧 Instant Payment Notification (IPN)

Instant Payment Notification (IPN) là thông điệp được gửi từ Nhà Cung Cấp Dịch Vụ Thanh Toán - Payment Service Provider (PSP) đến Bên Sử Dụng Dịch Vụ Thanh Toán - Payment Service Consumer (PSC). Việc này sử dụng giao thức HTTP và quy trình này là bất đồng bộ.

### Tại sao sử dụng IPN

Instant Payment Notification (IPN) được sử dụng để thông báo (notify) kết quả giao dịch <b>ngay lập tức</b> đến PSC:

- Thanh Toán: Web, SmartTv.
- Thanh Toán Trực Tuyến
- Thanh Toán Định Kỳ
  Đối tác có thể sử dụng hệ thống của mình để xử lý thông tin nhận được từ PSP:

- Cập nhật trạng thái giao dịch (khuyên dùng)
- Cập nhật số dư tài khoản (nạp tiền), xuất sản phẩm, thông báo kết quả giao dịch đến ứng dụng di động,...
- Gửi hóa đơn điện tử
- Sử dụng IPN để khắc phục trường hợp thanh toán thành công nhưng người dùng không nhận được sản phẩm.

**Lý do cần IPN:**

- Người dùng đóng trình duyệt
- Không thể điều hướng đến trang ban đầu của đối tác: do đường truyền, hệ thống quá tải,...

### Cách thực thi IPN

Đối tác cần tạo **API** phía backend để 'lắng nghe' kết quả từ **Pay2S** và cung cấp URL của API đó trong field ipnUrl.

Sau khi người dùng sử dụng thanh toán trên Pay2S (web hoặc app), Pay2S sẽ thông báo kết quả giao dịch **ngay lập tức** đến URL này. API của đối tác nhận kết quả thanh toán và tiếp tục xử lý trong hệ thống bên họ.

## 📋 Mô tả chi tiết các IPN Parameters

| Tham số | Loại | Mô tả |
|---------|------|-------|
| partnerCode | String | Mã đối tác |
| orderId | String | Mã đơn hàng |
| requestId | String | Mã yêu cầu |
| amount | Long | Số tiền giao dịch (đơn vị: VND) |
| orderInfo | String | Thông tin đơn hàng |
| orderType | String | Loại đơn hàng |
| transId | Long | ID giao dịch tại Pay2S |
| resultCode | Int | Mã kết quả (0=thành công) |
| message | String | Thông báo kết quả |
| payType | String | Phương thức thanh toán |
| responseTime | String | Thời gian phản hồi (format: yyyyMMddHHmmss) |
| extraData | String | Dữ liệu bổ sung từ merchant |
| m2signature | String | Chữ ký HMAC SHA256 xác thực |

## ⚠️ Lưu ý quan trọng

:::: warning CẢNH BÁO
1. **Xác thực chữ ký bắt buộc**: Đây là bước không thể bỏ qua để đảm bảo yêu cầu từ Pay2S chứ không phải kẻ xấu.
2. **Kiểm tra amount**: Đảm bảo số tiền nhận được khớp với số tiền lưu trong database.
3. **Kiểm tra orderId**: Giao dịch phải tồn tại trong hệ thống của bạn.
4. **Phản hồi trong 30 giây**: Tốc độ xử lý rất quan trọng, nếu quá chậm Pay2S sẽ retry.
5. **Retry logic**: Pay2S sẽ gửi lại tối đa 5 lần, nên cần xử lý idempotent (gọi nhiều lần được kết quả như nhau).
6. **Log tất cả**: Lưu lại raw data, signature, và kết quả xác thực để debug.
::::

## 📊 Quy trình xử lý IPN

```
1. Người dùng thanh toán thành công trên Pay2S
   ↓
2. Pay2S gửi HTTP POST đến ipnUrl
   ↓
3. Backend nhận JSON request
   ↓
4. Xác thực signature (HMAC SHA256)
   ↓
5. Nếu signature sai → Log lỗi → Trả về 200 OK
   ↓
6. Kiểm tra orderId tồn tại?
   ├─ Không → Trả về 200 OK
   └─ Có → Tiếp tục
   ↓
7. Kiểm tra amount khớp?
   ├─ Không → Log cảnh báo → Trả về 200 OK
   └─ Có → Tiếp tục
   ↓
8. Kiểm tra resultCode
   ├─ 0 (Thành công) → Cập nhật DB → Gửi sản phẩm/dịch vụ
   └─ ≠ 0 (Thất bại) → Cập nhật trạng thái thất bại
   ↓
9. Trả về { "success": true } với HTTP 200 OK
```

## 🔄 Retry Logic của Pay2S

Nếu nhận được status code khác 200 hoặc không có response trong 30 giây, Pay2S sẽ gửi lại:

- **Lần 1**: Ngay sau thanh toán
- **Lần 2**: 5 phút sau
- **Lần 3**: 15 phút sau
- **Lần 4**: 1 giờ sau
- **Lần 5**: 24 giờ sau

**Mẹo**: Lưu `(orderId, transId)` vào cache/DB với TTL 24 giờ để đếm số lần IPN nhận được.

## ❓ Câu hỏi thường gặp

**Q: Tại sao cần xác thực chữ ký?**
A: Để đảm bảo yêu cầu đích thực từ Pay2S, ngăn chặn fake request từ kẻ xấu.

**Q: Có thể nhận IPN nhiều lần không?**
A: Có, do network error hoặc timeout. Cần kiểm tra `(orderId, transId)` trước khi xử lý lần thứ 2.

**Q: Phải trả về gì?**
A: HTTP 200 OK + Body: `{"success": true}` (JSON) trong vòng 30 giây.

**Q: Nếu backend bị lỗi sao?**
A: Trả về HTTP 500 hoặc không trả về trong 30s, Pay2S sẽ retry.

**Q: resultCode là gì?**
A: Mã trạng thái giao dịch:
- `0` = Thành công
- `9000` = Đã được xác thực (authorization)
- Khác = Thất bại (xem bảng resultCode phổ biến trên)

**Q: Có cách test IPN không?**
A: Sử dụng Postman/curl gửi POST request tới endpoint của bạn với sample JSON ở trên.

## 🔐 Best Practices

1. **Luôn HTTPS**: Endpoint phải là HTTPS, không dùng HTTP
2. **Validate input**: Kiểm tra type, format của tất cả tham số
3. **Idempotent**: Xử lý lại IPN cũ không gây hậu quả
4. **Async processing**: Offload việc xử lý nặng vào queue (Redis, RabbitMQ)
5. **Monitoring**: Alert khi có lỗi xác thực signature hoặc amount mismatch
6. **Database transaction**: Dùng transaction để cập nhật trạng thái đơn hàng
7. **Timeout handling**: Set timeout 20-25s để kịp trả về trong 30s

---

Nguồn trang: https://docs.pay2s.vn/api/payment-notification.html

# Xử lý kết quả thanh toán (Payment Notification)

## 🔁 Giao diện Redirect (Client)

Sau khi luồng thanh toán hoàn tất, khách hàng được điều hướng đến `redirectUrl` mà bên đối tác đã cung cấp trong `create` request. Một vài thông số sẽ được thêm vào URL dưới dạng query parameters:

- **Method**: GET
- **Cấu trúc**: `redirectUrl?partnerCode=xxx&orderId=xxx&...`

### 📋 Query Parameters

| Parameter | Loại | Mô tả |
|-----------|------|-------|
| partnerCode | String | Mã đối tác |
| orderId | String | Mã đơn hàng |
| requestId | String | Mã yêu cầu |
| amount | String | Số tiền |
| orderInfo | String | Thông tin đơn hàng |
| orderType | String | Loại đơn hàng |
| transId | String | ID giao dịch (nếu có) |
| resultCode | String | Mã kết quả (0 = thành công) |
| message | String | Thông báo kết quả |
| payType | String | Phương thức thanh toán |
| responseTime | String | Thời gian phản hồi |
| m2signature | String | Chữ ký xác thực |

## ⚠️ Lưu ý quan trọng

:::: warning CẢNH BÁO
1. **Luôn xác thực chữ ký**: Đây là bước bắt buộc để đảm bảo yêu cầu đến từ Pay2S chứ không phải từ kẻ xấu.
2. **Kiểm tra Amount**: Đảm bảo số tiền nhận được khớp với số tiền lưu trong database của bạn.
3. **Xử lý idempotent**: Có thể nhận được redirect nhiều lần cho cùng một giao dịch - cần kiểm tra `orderId` trước khi xử lý.
4. **Timeout**: Nên xử lý nhanh chóng, không nên để người dùng chờ lâu.
5. **Log tất cả**: Lưu lại các tham số nhận được để debug sau này.
::::

## 💻 Code mẫu

::: code-group

```php [PHP]
<?php
header('Content-type: text/html; charset=utf-8');


$accessKey = '';
$secretKey = '';

if (!empty($_GET)) {
    $partnerCode = $_GET["partnerCode"];
    $orderId = $_GET["orderId"];
    $requestId = $_GET["requestId"];
    $amount = $_GET["amount"];
    $orderInfo = $_GET["orderInfo"];
    $orderType = $_GET["orderType"];
    $transId = $_GET["transId"] ?? '';
    $resultCode = $_GET["resultCode"];
    $message = $_GET["message"];
    $payType = $_GET["payType"];
    $responseTime = $_GET["responseTime"];
    $extraData = $_GET["extraData"] ?? '';
    $m2signature = $_GET["m2signature"]; // Pay2S signature

    // Tạo chuỗi hash để xác thực chữ ký
    $rawHash = "accessKey=$accessKey&amount=$amount&message=$message&orderId=$orderId&orderInfo=$orderInfo&orderType=$orderType&partnerCode=$partnerCode&payType=$payType&requestId=$requestId&responseTime=$responseTime&resultCode=$resultCode";
    $partnerSignature = hash_hmac("sha256", $rawHash, $secretKey);

    // Kiểm tra chữ ký
    if ($m2signature == $partnerSignature) {
        if ($resultCode == '0') {
            $result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>';
        } else {
            $result = '<div class="alert alert-danger"><strong>Payment status: </strong>' . $message . '</div>';
        }
    } else {
        $result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
    }
}

```
```js [NodeJS]
const express = require('express');
const crypto = require('crypto');

const app = express();
const port = 3000;

app.get('/redirect', (req, res) => {
    const accessKey = ''; // Thay bằng accessKey thực tế
    const secretKey = ''; // Thay bằng secretKey thực tế

    // Lấy các giá trị từ query parameters
    const partnerCode = req.query.partnerCode;
    const orderId = req.query.orderId;
    const requestId = req.query.requestId;
    const amount = req.query.amount;
    const orderInfo = req.query.orderInfo;
    const orderType = req.query.orderType;
    const transId = req.query.transId || '';
    const resultCode = req.query.resultCode;
    const message = req.query.message;
    const payType = req.query.payType;
    const responseTime = req.query.responseTime;
    const extraData = req.query.extraData || '';
    const m2signature = req.query.m2signature; // Pay2S signature

    // Tạo chuỗi rawHash
    const rawHash = `accessKey=${accessKey}&amount=${amount}&message=${message}&orderId=${orderId}&orderInfo=${orderInfo}&orderType=${orderType}&partnerCode=${partnerCode}&payType=${payType}&requestId=${requestId}&responseTime=${responseTime}&resultCode=${resultCode}`;

    // Tạo chữ ký HMAC SHA256
    const partnerSignature = crypto.createHmac('sha256', secretKey).update(rawHash).digest('hex');

    console.log('Debug rawHash:', rawHash);
    console.log('Debug partnerSignature:', partnerSignature);

    let result;

    // Kiểm tra chữ ký
    if (m2signature === partnerSignature) {
        if (resultCode === '0') {
            result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>';
        } else {
            result = `<div class="alert alert-danger"><strong>Payment status: </strong>${message}</div>`;
        }
    } else {
        result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
    }

    // Trả về kết quả
    res.send(result);
});

// Khởi động server
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

```
```js [ASP]
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

namespace Pay2SRedirect.Controllers
{
    public class PaymentController : Controller
    {
        // Hành động xử lý redirect
        [HttpGet("redirect")]
        public IActionResult RedirectFromPay2S(
            string partnerCode, string orderId, string requestId, 
            string amount, string orderInfo, string orderType, 
            string transId, string resultCode, string message, 
            string payType, string responseTime, string extraData, 
            string m2signature)
        {
            string accessKey = "";  // Thay thế với accessKey thực tế
            string secretKey = "";  // Thay thế với secretKey thực tế

            // Tạo chuỗi rawHash cho chữ ký
            var rawHash = $"accessKey={accessKey}&amount={amount}&message={message}&orderId={orderId}&orderInfo={orderInfo}&orderType={orderType}&partnerCode={partnerCode}&payType={payType}&requestId={requestId}&responseTime={responseTime}&resultCode={resultCode}";

            // Tạo chữ ký HMAC SHA256
            var partnerSignature = CreateHmacSha256Signature(rawHash, secretKey);

            // Kiểm tra chữ ký
            if (m2signature == partnerSignature)
            {
                if (resultCode == "0")
                {
                    // Trả về kết quả thành công
                    ViewBag.Result = "<div class='alert alert-success'><strong>Payment status: </strong>Success</div>";
                }
                else
                {
                    // Trả về kết quả lỗi
                    ViewBag.Result = $"<div class='alert alert-danger'><strong>Payment status: </strong>{message}</div>";
                }
            }
            else
            {
                // Thông báo chữ ký không hợp lệ
                ViewBag.Result = "<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>";
            }

            // Trả về view chứa kết quả
            return View();
        }

        // Hàm tạo chữ ký HMAC SHA256
        private string CreateHmacSha256Signature(string data, string secretKey)
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
            {
                byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
                return BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }
    }
}
```
```python [Python]
from flask import Flask, request, render_template_string
import hmac
import hashlib

app = Flask(__name__)

def verify_signature(params, signature, secret_key):
    """Xác thực chữ ký HMAC SHA256"""
    raw_hash = f"accessKey={params.get('accessKey')}&amount={params.get('amount')}&message={params.get('message')}&orderId={params.get('orderId')}&orderInfo={params.get('orderInfo')}&orderType={params.get('orderType')}&partnerCode={params.get('partnerCode')}&payType={params.get('payType')}&requestId={params.get('requestId')}&responseTime={params.get('responseTime')}&resultCode={params.get('resultCode')}"
    
    computed_signature = hmac.new(
        secret_key.encode('utf-8'),
        raw_hash.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return computed_signature == signature

@app.route('/redirect')
def redirect_from_pay2s():
    access_key = ""  # Khóa truy cập
    secret_key = ""  # Khóa bí mật
    
    # Lấy các giá trị từ query parameters
    params = request.args.to_dict()
    
    partner_code = params.get('partnerCode')
    order_id = params.get('orderId')
    request_id = params.get('requestId')
    amount = params.get('amount')
    order_info = params.get('orderInfo')
    order_type = params.get('orderType')
    trans_id = params.get('transId', '')
    result_code = params.get('resultCode')
    message = params.get('message')
    pay_type = params.get('payType')
    response_time = params.get('responseTime')
    extra_data = params.get('extraData', '')
    m2signature = params.get('m2signature')  # Pay2S signature
    
    # Xác thực chữ ký
    if verify_signature(params, m2signature, secret_key):
        if result_code == '0':
            result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>'
        else:
            result = f'<div class="alert alert-danger"><strong>Payment status: </strong>{message}</div>'
    else:
        result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>'
    
    return render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Payment Result</title>
            <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
        </head>
        <body>
            <div class="container mt-5">
                {{ result | safe }}
            </div>
        </body>
        </html>
    ''', result=result)

if __name__ == '__main__':
    app.run(port=3000, debug=True)
```
```java [Java]
import com.google.gson.Gson;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

@Controller
public class Pay2SRedirectController {
    
    private static String createSignature(String rawData, String secretKey) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8),
            0,
            secretKey.getBytes(StandardCharsets.UTF_8).length,
            "HmacSHA256"
        );
        mac.init(secretKeySpec);
        byte[] hash = mac.doFinal(rawData.getBytes(StandardCharsets.UTF_8));
        
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }
    
    @GetMapping("/redirect")
    public String redirectFromPay2S(
            @RequestParam String partnerCode,
            @RequestParam String orderId,
            @RequestParam String requestId,
            @RequestParam String amount,
            @RequestParam String orderInfo,
            @RequestParam String orderType,
            @RequestParam(required = false) String transId,
            @RequestParam String resultCode,
            @RequestParam String message,
            @RequestParam String payType,
            @RequestParam String responseTime,
            @RequestParam(required = false) String extraData,
            @RequestParam String m2signature,
            Model model) {
        
        String accessKey = "";  // Khóa truy cập
        String secretKey = "";  // Khóa bí mật
        
        try {
            // Tạo chuỗi rawHash
            String rawHash = String.format(
                "accessKey=%s&amount=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s",
                accessKey, amount, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode
            );
            
            // Tạo chữ ký
            String partnerSignature = createSignature(rawHash, secretKey);
            
            String result;
            
            // Kiểm tra chữ ký
            if (m2signature.equals(partnerSignature)) {
                if ("0".equals(resultCode)) {
                    result = "<div class='alert alert-success'><strong>Payment status: </strong>Success</div>";
                } else {
                    result = "<div class='alert alert-danger'><strong>Payment status: </strong>" + message + "</div>";
                }
            } else {
                result = "<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>";
            }
            
            model.addAttribute("result", result);
        } catch (Exception e) {
            model.addAttribute("result", "<div class='alert alert-danger'>Error: " + e.getMessage() + "</div>");
        }
        
        return "payment-result";
    }
}
```
```go [Go]
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"fmt"
	"html/template"
	"log"
	"net/http"
)

func createSignature(rawData, secretKey string) string {
	h := hmac.New(sha256.New, []byte(secretKey))
	h.Write([]byte(rawData))
	return fmt.Sprintf("%x", h.Sum(nil))
}

func redirectHandler(w http.ResponseWriter, r *http.Request) {
	accessKey := ""  // Khóa truy cập
	secretKey := ""  // Khóa bí mật
	
	// Parse query parameters
	err := r.ParseForm()
	if err != nil {
		http.Error(w, "Error parsing form", http.StatusBadRequest)
		return
	}
	
	partnerCode := r.FormValue("partnerCode")
	orderId := r.FormValue("orderId")
	requestId := r.FormValue("requestId")
	amount := r.FormValue("amount")
	orderInfo := r.FormValue("orderInfo")
	orderType := r.FormValue("orderType")
	resultCode := r.FormValue("resultCode")
	message := r.FormValue("message")
	payType := r.FormValue("payType")
	responseTime := r.FormValue("responseTime")
	m2signature := r.FormValue("m2signature")
	
	// Tạo chuỗi rawHash
	rawHash := fmt.Sprintf(
		"accessKey=%s&amount=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s",
		accessKey, amount, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode,
	)
	
	// Tạo chữ ký
	partnerSignature := createSignature(rawHash, secretKey)
	
	var result template.HTML
	
	// Kiểm tra chữ ký
	if m2signature == partnerSignature {
		if resultCode == "0" {
			result = template.HTML("<div class='alert alert-success'><strong>Payment status: </strong>Success</div>")
		} else {
			result = template.HTML(fmt.Sprintf("<div class='alert alert-danger'><strong>Payment status: </strong>%s</div>", message))
		}
	} else {
		result = template.HTML("<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>")
	}
	
	// Trả về HTML
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	tmpl := template.Must(template.New("result").Parse(`
		<!DOCTYPE html>
		<html>
		<head>
			<title>Payment Result</title>
			<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
		</head>
		<body>
			<div class="container mt-5">
				{{ . }}
			</div>
		</body>
		</html>
	`))
	tmpl.Execute(w, result)
}

func main() {
	http.HandleFunc("/redirect", redirectHandler)
	log.Println("Server running on port 3000")
	log.Fatal(http.ListenAndServe(":3000", nil))
}
```
```ruby [Ruby]
require 'sinatra'
require 'openssl'
require 'erb'

def verify_signature(params, signature, secret_key)
  raw_hash = "accessKey=#{params['accessKey']}&amount=#{params['amount']}&message=#{params['message']}&orderId=#{params['orderId']}&orderInfo=#{params['orderInfo']}&orderType=#{params['orderType']}&partnerCode=#{params['partnerCode']}&payType=#{params['payType']}&requestId=#{params['requestId']}&responseTime=#{params['responseTime']}&resultCode=#{params['resultCode']}"
  
  computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_hash)
  computed_signature == signature
end

get '/redirect' do
  access_key = ''  # Khóa truy cập
  secret_key = ''  # Khóa bí mật
  
  partner_code = params['partnerCode']
  order_id = params['orderId']
  request_id = params['requestId']
  amount = params['amount']
  order_info = params['orderInfo']
  order_type = params['orderType']
  trans_id = params['transId'] || ''
  result_code = params['resultCode']
  message = params['message']
  pay_type = params['payType']
  response_time = params['responseTime']
  extra_data = params['extraData'] || ''
  m2signature = params['m2signature']
  
  # Xác thực chữ ký
  if verify_signature(params, m2signature, secret_key)
    if result_code == '0'
      result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>'
    else
      result = "<div class=\"alert alert-danger\"><strong>Payment status: </strong>#{message}</div>"
    end
  else
    result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>'
  end
  
  erb :payment_result, locals: { result: result }
end
```
:::

## 📋 Quy trình xử lý Redirect

```
1. Người dùng thanh toán thành công trên Pay2S
   ↓
2. Pay2S chuyển hướng người dùng đến redirectUrl với query parameters
   ↓
3. Backend nhận các tham số từ URL
   ↓
4. Xác thực chữ ký m2signature
   ↓
5. Kiểm tra resultCode:
   - 0: Giao dịch thành công → Cập nhật database
   - ≠ 0: Giao dịch thất bại → Hiển thị thông báo lỗi
   ↓
6. Hiển thị kết quả cho người dùng
```

## 🔍 Kết hợp với IPN

- **Redirect**: Thông báo ngay cho người dùng thấy kết quả trên browser
- **IPN**: Thông báo cho backend để xử lý tự động và lưu database

Khuyến cáo: Sử dụng cả hai để đảm bảo không bị mất thông tin giao dịch.

## ❓ Câu hỏi thường gặp

**Q: Tại sao cần xác thực chữ ký?**  
A: Để đảm bảo yêu cầu đích thực đến từ Pay2S, không phải từ kẻ xấu giả mạo.

**Q: Điều gì xảy ra nếu người dùng không nhận được redirect?**  
A: Pay2S sẽ gửi IPN để backend xử lý, sau đó người dùng có thể kiểm tra trạng thái thủ công.

**Q: Có thể nhận được redirect nhiều lần không?**  
A: Có, do mạng hoặc lỗi khác. Vì thế cần kiểm tra `orderId` trước khi xử lý.

**Q: resultCode là gì?**  
A: Mã trạng thái giao dịch:
- `0` = Thành công
- `9000` = Đã được xác thực (authorization)
- `>0` = Thất bại (xem IPN doc để biết mã lỗi cụ thể)



---

Nguồn trang: https://docs.pay2s.vn/api/simulate-payment.html

# Mô phỏng thanh toán đơn demo

Tự động kiểm thử **Create đơn demo → mô phỏng thanh toán → nhận IPN**. Dùng ngay **Secret Key của tài khoản demo** làm Bearer token; không cần token mới hoặc ký HMAC request.

## Request

```http
POST https://payment.pay2s.vn/v1/gateway/api/simulate-payment
Authorization: Bearer YOUR_DEMO_SECRET_KEY
Content-Type: application/json
```

```json
{
  "orderId": "ORDER_123",
  "resendIpn": false
}
```

- `orderId`: mã đã gửi lúc create, không phải mã PAY2SJSC; bắt buộc là chuỗi.
- `resendIpn`: boolean tùy chọn, mặc định `false`. Đặt `true` để chủ động gửi lại IPN của đơn demo đã thanh toán.
- Không cần gửi `accessKey`, `partnerCode`, `requestId`, `requestType` hay `signature`. Không gửi Secret Key trong URL/body, không Base64 hay HMAC khóa.
- Số tiền lấy từ đơn đã lưu; không truyền `amount` để thay đổi số tiền.

## Điều kiện

Create vẫn dùng [API hiện tại](/api/collection-link). Response create phải có `environment: "demo"`: tài khoản là demo và tất cả ngân hàng được chọn đều là demo. Server kiểm tra tài khoản, ngân hàng nhận và cờ demo đã lưu của đơn; không tin cờ môi trường do client tự gửi.

Đơn phải thuộc phạm vi Secret Key, có `ipnUrl` và đang `pending` hoặc đã thanh toán demo trước đó. Đơn production, đơn đã hủy và `orderId` trùng nhiều đơn bị từ chối. Secret Key cửa hàng demo chỉ truy cập đơn cửa hàng đó; Secret Key tài khoản truy cập đơn không gắn cửa hàng, cùng phạm vi create.

Không chuyển tiền thật. API giữ nguyên dữ liệu/yêu cầu hóa đơn từ create, không tự thêm yêu cầu xuất hóa đơn. Luồng hóa đơn demo phụ thuộc cấu hình sandbox.

## Postman

Import [collection và Environment mẫu](/others/postman). Điền `secretKey`, `orderId` rồi chọn:

- **Simulate Payment**: mô phỏng thanh toán, không ép gửi lại IPN nếu đã thanh toán.
- **Resend Demo IPN**: cùng endpoint, với `resendIpn: true` để test lại callback/trigger.

Cả hai đọc Secret Key từ Environment, đặt header Authorization và tạo body. Không có script ký HMAC cho hai request này. Create, Cancel và API HĐĐT vẫn giữ cách xác thực riêng.

## Response

```json
{
  "status": true,
  "message": "Đã ghi nhận giao dịch demo và khớp đơn thành công.",
  "data": {
    "partnerCode": "YOUR_PARTNER_CODE",
    "orderId": "ORDER_123",
    "invoiceNumber": "PAY2SJSC...",
    "environment": "demo",
    "orderStatus": "completed",
    "transactionNumber": "DEMO-...",
    "alreadyPaid": false,
    "ipnRequeued": false,
    "ipnStatus": "queued"
  }
}
```

`alreadyPaid: true` nghĩa là đơn đã thanh toán trước đó; không có giao dịch mới. `ipnRequeued: true` nghĩa là request này đã đặt lại hàng chờ IPN. `ipnStatus` có thể là `queued`, `sending`, `sent`, `retrying`, `failed` tại thời điểm trả kết quả.

## Gửi và gửi lại IPN

Worker gửi POST tới `ipnUrl` của đơn. **IPN vẫn có chữ ký `m2signature` như thanh toán thật**; chỉ request giả lập được đơn giản hóa. Kiểm tra theo [chữ ký IPN](/others/signature#xác-thực-ipn), phản hồi HTTP 200 và `{"success":true}` khi xử lý xong.

IPN gửi bất đồng bộ; response mô phỏng thành công không đảm bảo callback đã tới. Bộ test cần đợi IPN với thời gian chờ phù hợp. Đối chiếu bằng `orderId`; IPN hiện dùng `requestId` là ID nội bộ, `requestTrace` là requestId từ create.

- Gọi lại thông thường: không tạo giao dịch mới, không ép gửi lại IPN.
- `resendIpn: true`: nếu đơn đã thanh toán và IPN đã gửi/đang retry/hết lượt retry, đưa lại vào hàng chờ.
- IPN đang chờ hoặc đang gửi: giữ lượt hiện có, không tạo lượt song song.
- Đơn pending: thanh toán mô phỏng và gửi IPN lần đầu.

Mỗi lần gọi `resendIpn: true` sau khi lượt trước hoàn tất có thể tạo callback mới. Không dùng cờ này cho retry thanh toán thông thường. Callback giữ mã đơn/giao dịch nhưng thời gian và chữ ký có thể khác; phía nhận phải tránh cộng tiền/kích hoạt dịch vụ trùng.

## Mã lỗi

| HTTP | Ý nghĩa |
| --- | --- |
| 400 | JSON, orderId hoặc kiểu resendIpn không hợp lệ |
| 401 | Thiếu/sai Secret Key |
| 403 | Tài khoản hoặc đơn production |
| 404 | Không có đơn trong phạm vi khóa |
| 409 | Đơn đã hủy, ngân hàng demo không hợp lệ, thiếu ipnUrl hoặc mã đơn bị trùng |
| 500 | Lỗi xử lý; có thể thử lại cùng orderId |

Không tìm thấy đơn thì báo lỗi; không tạo giao dịch độc lập thay thế.


---

Nguồn trang: https://docs.pay2s.vn/api/tich-hop-ky-thuat.html

# 🧩 Tích hợp kỹ thuật

## Thông tin môi trường sandbox

Môi trường sandbox dùng trực tiếp trên môi trường Productions, chỉ khác nhau về tài khoản sandbox và tài khoản thật. Nếu quý khách có nhu cầu cần tài khoản sandbox riêng có thể liên hệ bộ phận kỹ thuật qua kỹ thuật để được cung cấp.

| Key          | Value                                                    |
| ------------ | -------------------------------------------------------- |
| **Website** | `https://pay2s.vn`                     |
| **Tài khoản - mật khẩu** | pay2s - 123456789                     |
| **Method**       | `POST`                                                 |
| **API EndPoint**       | **Sandbox:** `https://payment.pay2s.vn` |
| **Tạo giao dịch ảo**       | `https://pay2s.vn/client/demo-transfer` |
| **Khởi tạo IPN**       | `https://pay2s.vn/client/demo-ipn-test` |

## 🔑 Thông tin tích hợp

Pay2S cung cấp bộ mã định danh dùng để tích hợp hệ thống:

- **Partner Code**: Định danh duy nhất cho tài khoản Pay2S của bạn.
- **Access Key**: Mã dùng để cấp quyền truy cập API.
- **Secret Key**: Khoá bí mật dùng để tạo **digital signature** trong quá trình xác thực yêu cầu.

> ⚠️ **Bảo mật**: Không chia sẻ Access Key và Secret Key cho bất kỳ ai. Bạn nên lưu trữ các thông tin này ở nơi an toàn và chỉ dùng trên server backend.

![Tích hợp kỹ thuật](/assets/tich-hop-thanh-toan.png)

---

## 🔄 Luồng xử lý

1. Khách hàng thực hiện mua hàng trên Website hoặc ứng dụng của merchant và lựa chọn thanh toán trực tuyến Napas 247 cho đơn hàng.
2. Website hoặc ứng dụng của merchant tiến hành gọi tạo [link thanh toán ](/api/collection-link.html), Pay2S sẽ kiểm tra dữ liệu và trả về kết quả chứa link thanh toán. Khi hệ thống của merchant nhận kết quả link thanh toán cần chuyển hướng khách hàng của bạn đến trang checkout của Pay2S bằng cách mở link thanh toán từ kết quả.
3. Khách hàng sử dụng ứng dụng ngân hàng để quét mã VietQR từ link thanh toán.
4. Giao dịch ghi nhận thành công tại ngân hàng, Pay2S sẽ trả kết quả thành công về redirectUrl gồm: trạng thái, mã đơn hàng, mã link thanh toán, ... Từ kết quả nhận được trên redirectUrl Website hoặc ứng dụng của merchant hiển thị giao diện thành công.
5. Sau khi có kết quả ở giao diện, đồng thời Pay2S sẽ gửi một kết quả với đầy đủ thông tin thanh toán tới ipnUrl của cửa hàng, sau đó merchant cập nhật trạng thái đơn hàng phù hợp.

## ⚙️ Cấu hình HTTP Request

| Key          | Value                                                    |
| ------------ | -------------------------------------------------------- |
| **Content-Type** | `application/json; charset=UTF-8`                       |
| **Method**       | `POST`                                                 |
| **Domain**       | **Production:** `https://payment.pay2s.vn`
 |               |    **Sandbox:** `https://sandbox-payment.pay2s.vn`             |

---

## 🧾 Mã giao dịch Pay2S (`trans_id`)

Mỗi giao dịch tại Pay2S được gán một **mã giao dịch duy nhất (`trans_id`)**.  
Đây là mã do hệ thống Pay2S tự sinh và dùng để truy vết, kiểm tra và đối soát giao dịch.

---

## 📦 Mã đơn hàng đối tác (`orderId`)

Đây là mã giao dịch bạn tự định nghĩa bên phía hệ thống của mình để xác định đơn hàng.

> ✅ **Quy định đặt mã đơn hàng:**
>
> - Không được để trống  
> - Không vượt quá **64 byte**  
> - Không chứa thông tin **bí mật** hoặc **nhạy cảm**  

`orderId` nên mang ý nghĩa định danh rõ ràng và có thể liên kết tới đơn hàng trong hệ thống của bạn.

## 📦 Mã thông tin đơn hàng (`orderInfo`)

Đây là mã chuyển tièn bạn tự định nghĩa bên phía hệ thống của mình để khách hàng chuyển khoản theo nội dung này.

Lưu ý: Giới hạn 10-32 ký tự, chỉ chấp nhậ ký tự chữ + số, không dấu gạch ngang hoặc đặc biêt.

> ✅ **Quy định đặt mã đơn hàng:**
> 
  - Không được trùng lặp giữa các đơn hàng.
> - Không được để trống.  
> - Không vượt quá **64 byte**  
> - Không chứa thông tin **bí mật** hoặc **nhạy cảm**  

`orderInfo` nên mang ý nghĩa định danh rõ ràng và không được chứa các kí tự không được phép xuất hiện trong nội dung chuyển tiền của ngân hàng.
---

🎯 **Tiếp theo**: [Cách tạo chữ ký số (Signature)](/others/signature.html)


---

Nguồn trang: https://docs.pay2s.vn/others/signature.html

# 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ác | Trường chữ ký | Hướng xử lý |
| --- | --- | --- |
| [Tạo đơn V1](/api/collection-link) | `signature` | Merchant ký request gửi Pay2S |
| [Tạo đơn V2](/api/collection-link-v2) | `signature` | Ký thêm `extraData` và `signatureVersion=2` |
| [Hủy đơn](/api/cancel-order) | `signature` | Ký riêng với `requestType=cancel` |
| [Mô phỏng thanh toán demo](/api/simulate-payment) | `signature` | Ký riêng với `requestType=simulatePayment` |
| [Nhận IPN](/api/instant-payment-notification) | `m2signature` | Merchant 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ũ.

::: info 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](/api/collection-link-v2).

::: warning 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='YOUR_ACCESS_KEY'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](/api/collection-link-v2-sdk) để 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](/api/cancel-order) để 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/simulate-payment). 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ượng | Kiểm tra |
| --- | --- |
| V1 cũ ngừng khớp | Khô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àng | Raw create dùng `bankAccounts=Array`, không JSON.stringify mảng |
| Cùng dữ liệu nhưng chữ ký khác | Kiể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 được | Bộ 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ớp | Dùng `m2signature` và chuỗi IPN; không thay bằng chuỗi create |
| Hủy đơn không khớp | Ký 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.

