# KLK merchant integration

Call KLK from **your server only**. Base URL: `https://pay.klicksev.net`

Before integration, send KLK this information in a **private message**:

1. Merchant website
2. Merchant account name
3. Binding / whitelist IP
   - a. Backoffice whitelist
   - b. Request Settlement whitelist
   - c. API whitelist
4. Your **RSA public key (1024 bit)** only

Generate the key pair on your server. Keep the private key. Send only the public key.

```bash
openssl genrsa -out merchant-private.pem 1024
openssl rsa -in merchant-private.pem -pubout -out merchant-public.pem
```

KLK then gives you two values **once**:

- **AES key** — encrypt and decrypt the `data` parameter
- **Payment RSA public key** — verify KLK's signature on the response and on the callback

| Key | Who creates it | Who keeps it | Use |
|---|---|---|---|
| AES key | KLK | You and KLK | Encrypt and decrypt `data` |
| Merchant RSA private key | You | Your server only | Sign each request |
| Merchant RSA public key | You | Send to KLK | KLK verifies your signature |
| Payment RSA public key | KLK | You | Verify KLK's signature |
| Payment RSA private key | KLK | KLK only | KLK signs the response and the callback |

---

## Call format

Every deposit and system withdrawal request is JSON:

```json
{
  "merchant": "Merchant Name",
  "data": "<base64>",
  "signature": "<base64>"
}
```

`merchant` is the merchant account name you sent to KLK.

### AES

- Algorithm: `AES-128-CBC`
- Key: the AES key from KLK, base64, 16 bytes
- `data`: base64 of `IV (16 bytes) + ciphertext`
- Padding: PKCS7
- A new random IV is used for every `data` value

### Signature

- Algorithm: `RSA-SHA256` (SHA256withRSA)
- Sign the exact `data` string, not the plaintext
- Use your merchant RSA private key
- `signature` is base64
- KLK checks it with the RSA public key you provided

Put `timestamp` in every plaintext JSON. It is Unix time in seconds. KLK rejects it when it is missing or more than 5 minutes from KLK's clock.

KLK replies with the same envelope. Verify `signature` with the **payment RSA public key**, then decrypt `data` with the AES key.

```js
const crypto = require('crypto');
const aesKey = Buffer.from(AES_KEY_BASE64, 'base64');

function encryptData(obj) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-128-cbc', aesKey, iv);
  const body = Buffer.concat([cipher.update(JSON.stringify(obj), 'utf8'), cipher.final()]);
  return Buffer.concat([iv, body]).toString('base64');
}

function signData(data) {
  return crypto.createSign('RSA-SHA256').update(data, 'utf8').sign(merchantPrivatePem, 'base64');
}

function verifyPayment(data, signature) {
  return crypto.createVerify('RSA-SHA256').update(data, 'utf8').verify(paymentPublicPem, signature, 'base64');
}

function decryptData(data) {
  const raw = Buffer.from(data, 'base64');
  const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, raw.subarray(0, 16));
  const text = Buffer.concat([decipher.update(raw.subarray(16)), decipher.final()]).toString('utf8');
  return JSON.parse(text);
}
```

### IP binding

| Binding | Used for |
|---|---|
| API whitelist | Deposit |
| Request Settlement whitelist | System withdrawal |
| Backoffice whitelist | Saved for backoffice binding |

An empty list allows any IP. After the first IP is saved, only those IPs can call that binding.

---

## Setup

### Deposit

1. Send KLK your website, merchant account name, the three IP lists, and your RSA public key.
2. Save the AES key and the payment RSA public key.
3. From your server, encrypt `payType`, `amount`, `refId`, and `timestamp`. Sign `data`.
4. Show the payer the QR in the decrypted response, or open the pay page. The payer must use `remarks` as the transfer content. The pay page does not use the signature.
5. Save `transactionId`. Read the result with that Deposit ID until `status` is `SUCCESS`, `FAILED`, or `CANCELLED`.
6. Deposit has no webhook.

### System Withdrawal

1. Prepare a public **https** webhook URL on your server. Localhost and private addresses are not accepted.
2. Send KLK your website, merchant account name, the three IP lists, your RSA public key, and the webhook URL. New merchants start in **Testing**.
3. Save the AES key and the payment RSA public key. They are shown once.
4. Ask KLK to switch the merchant to **Production** when testing is done.
5. Encrypt only `bank`, `accountName`, `accountNumber`, `withdrawalId`, `amount`, and `timestamp`.
6. Do not send `accountCode`. KLK uses the account assigned to this merchant.
7. Do not send `remarks`. KLK creates `CHUYEN KHOAN` plus the last 6 digits of `withdrawalId`.
8. Save the decrypted System Withdrawal `id`. Wait for the webhook. Do not poll.
9. On the webhook, verify the payment RSA signature, decrypt `data`, then reply **HTTP 2xx**.

---

## Deposit

`POST /api/create-deposit`

### 1. Plaintext inside `data`

```json
{
  "timestamp": 1755531600,
  "payType": "QR Pay",
  "amount": 1500000,
  "refId": "DP12345"
}
```

| Field | Required | Notes |
|---|---|---|
| `timestamp` | yes | Unix seconds |
| `payType` | yes | `QR Pay` or `Bank Transfer` |
| `amount` | yes | Number, max 200,000,000 |
| `refId` | yes | Your deposit order id |

### 2. What you receive

Decrypt `data`. The plaintext looks like this:

```json
{
  "ok": true,
  "deposit": {
    "transactionId": "D1",
    "refId": "DP12345",
    "status": "PENDING",
    "amount": 1500000,
    "bank": "MBB",
    "accountName": "NGUYEN VAN A",
    "accountNumber": "0123456789",
    "remarks": "KLK remark",
    "qrPayload": "000201..."
  },
  "payPath": "/m/deposit/D1"
}
```

Show `qrPayload` to the payer, or open `https://pay.klicksev.net` + `payPath`. The payer must use `remarks` as the transfer content. Save `transactionId`.

### 3. Deposit result

There is no deposit webhook. `POST /api/get-deposit-page` with this plaintext inside `data`:

```json
{ "timestamp": 1755531600, "depositId": "D1" }
```

Decrypt the response:

```json
{
  "ok": true,
  "deposit": {
    "transactionId": "D1",
    "refId": "DP12345",
    "status": "SUCCESS",
    "amount": 1500000
  }
}
```

| `status` | Result |
|---|---|
| `PENDING` | Still open |
| `SUCCESS` | Paid |
| `FAILED` | Failed |
| `CANCELLED` | Cancelled |

Finished when `status` is `SUCCESS`, `FAILED`, or `CANCELLED`. Match `refId` to your order. KLK returns only that merchant's deposit.

---

## System Withdrawal

`POST /api/call`

### 1. Plaintext inside `data`

```json
{
  "timestamp": 1755531600,
  "bank": "MBBank",
  "accountName": "NGUYEN VAN A",
  "accountNumber": "0123456789",
  "withdrawalId": "WD12345",
  "amount": 1500000
}
```

| Field | Required | Notes |
|---|---|---|
| `timestamp` | yes | Unix seconds |
| `bank` | yes | One name from the bank list below |
| `accountName` | yes | Recipient name |
| `accountNumber` | yes | Digits only |
| `withdrawalId` | yes | Your unique payout id. Must include digits. Letters, numbers, `.` `_` `:` `-` only. Max 64 characters. |
| `amount` | yes | Number, max 200,000,000 |

Do not send `accountCode`. KLK uses the account assigned to this merchant.

Do not send `remarks`. KLK creates it from `withdrawalId`: `CHUYEN KHOAN` plus the last 6 digits. Example: `WD12345` becomes `CHUYEN KHOAN 12345`.

Send `bank` exactly as one of these names:

| Bank | Bank | Bank | Bank |
|---|---|---|---|
| ACB | ABBank | Agribank | BacABank |
| BaoVietBank | BIDV | CAKE | CBBank |
| CIMB | Citibank | COOPBANK | DBSBank |
| Eximbank | GPBank | HDBank | HongLeong |
| HSBC | IBK HCM | IBK HN | IndovinaBank |
| KBank | KEBHana HCM | KEBHANA HN | KienLongBank |
| Kookmin HCM | Kookmin HN | Liobank | LPBank |
| MAFC | MBBank | MBV | MoMo |
| MSB | NamABank | NCB | Nonghyup |
| OCB | PGBank | PublicBank | PVcomBank |
| PVcomBank Pay | SaigonBank | Sacombank | SCB |
| SeABank | SHB | ShinhanBank | StandardChartered |
| Techcombank | Timo | TPBank | Ubank |
| UOB | VBSP | VIB | VietCapitalBank |
| Vietcombank | VietinBank | ViettelMoney | VietABank |
| VietBank | Vikki | Vikki by HDbank | VNPTMoney |
| VRB | VPBank | Woori | |

This creates a System Withdrawal. Do not poll. Wait for the callback.

### 2. What you receive

Decrypt `data`:

```json
{
  "ok": true,
  "withdrawal": {
    "id": "S1",
    "refId": "WD12345",
    "bank": "MBBank",
    "accountName": "NGUYEN VAN A",
    "accountNumber": "0123456789",
    "remarks": "CHUYEN KHOAN 12345",
    "process": "PENDING"
  }
}
```

`id` is KLK's System Withdrawal ID. `refId` is your `withdrawalId`. `bank`, `accountName`, and `accountNumber` are the recipient you sent.

### 3. Callback result

After KLK marks Paid or Fail, KLK `POST`s the same envelope to your webhook. Verify `signature` with the payment RSA public key, then decrypt `data`.

```json
{
  "event": "process",
  "id": "WD12345",
  "systemId": "S1",
  "process": "PAID",
  "amount": 1500000,
  "failReason": ""
}
```

| `process` | Result |
|---|---|
| `PAID` | Paid |
| `REJECT` | Fail. `failReason` is set. |

`id` is your `withdrawalId`. `systemId` is KLK's System Withdrawal ID. The payout is finished when `process` is `PAID` or `REJECT`.

Reply **HTTP 2xx**. KLK retries once if your webhook fails.

---

## Error checklist

A failed request returns JSON. `code` is the error code.

```json
{
  "ok": false,
  "code": "KLK1001",
  "message": "Merchant, data, and signature are required."
}
```

Deposit uses HTTP 400. Signature, timestamp, data, whitelist, and a missing envelope use HTTP 401. System withdrawal returns HTTP 500 with `code` and `message`.

| Error code | Error description |
|---|---|
| `KLK1001` | The request is missing `merchant`, `data`, or `signature`. |
| `KLK1002` | The signature does not match `data` and your RSA public key. |
| `KLK1003` | `data` is not valid AES-128-CBC ciphertext, or the plaintext is not a JSON object. |
| `KLK1004` | The plaintext has no `timestamp`. |
| `KLK1005` | `timestamp` is missing or more than 5 minutes from KLK's clock. |
| `KLK1006` | `merchant` does not match a KLK merchant account name. |
| `KLK1007` | KLK has turned this merchant off. |
| `KLK1008` | KLK does not have your RSA public key yet. |
| `KLK1009` | The caller IP is not on the API whitelist (deposit) or the Request Settlement whitelist (system withdrawal). |
| `KLK1010` | KLK could not read the caller IP. |
| `KLK1011` | Deposit plaintext is missing `payType` or `amount`. |
| `KLK1012` | Deposit `amount` is above 200,000,000. |
| `KLK1013` | KLK has no active deposit account for this merchant. |
| `KLK1014` | Deposit status plaintext is missing `depositId`. |
| `KLK1015` | That Deposit ID does not exist for this merchant. |
| `KLK1016` | System withdrawal is missing `bank`, `accountName`, `accountNumber`, or `amount`. |
| `KLK1017` | `bank` is not one of the names in the bank list. |
| `KLK1018` | System withdrawal is missing `withdrawalId`. |
| `KLK1019` | `withdrawalId` has no digits, so KLK cannot create `remarks`. |
| `KLK1020` | `withdrawalId` is longer than 64 characters. |
| `KLK1021` | `withdrawalId` is `.`, `..`, or contains `/`. |
| `KLK1022` | `withdrawalId` has a character KLK does not allow. |
| `KLK1023` | This `withdrawalId` was already used. |
| `KLK1024` | The same payout was sent again. |
| `KLK1025` | KLK will not pay this account number. |
| `KLK1026` | KLK has no in-shift payout account for this merchant. |
| `KLK1027` | System withdrawal `amount` is above 200,000,000. |
| `KLK1028` | More than 30 system withdrawals were created in one minute. |
| `KLK1029` | The decrypted request is not a system withdrawal. |
