Payment Gateway API
Integrate deposits, payouts, and real-time webhooks into your platform with our simple REST API. Fast, secure, and reliable.
Authentication
All API requests must include the following headers for authentication.
| Header | Value | Description |
|---|---|---|
| X-API-Key | your_api_key |
Your API key from the Client Management page |
| X-API-Secret | your_api_secret |
Your API secret from the Client Management page |
| Content-Type | application/json |
Request body format |
Idempotency — Prevent Duplicate Transactions
Every Deposit/Payout create request must include the X-Idempotency-Key header to prevent duplicate transactions caused by retries or double-submits.
X-Idempotency-Key — Required header on create endpoints
- X-Idempotency-Key: use a UUID v4 — a string unique per transaction, up to 256 characters (generated by the client).
- 1 transaction = 1 key; never reuse a key for a different transaction.
- A retry must reuse the same key + identical body (every byte) → the server replays the original response without creating a duplicate.
- Keys live for 60 minutes; after expiry the request is treated as new.
Error Responses
| HTTP | Code | When |
|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED |
X-Idempotency-Key header was not sent |
| 409 | IDEMPOTENCY_IN_PROGRESS |
The original request is still processing — wait, then retry with the same key |
| 422 | IDEMPOTENCY_BODY_MISMATCH |
Same key but a different body — a client bug; use a new key |
Deposit API
Create deposit requests to receive funds from customers via QR code or bank transfer.
Request Fields
| Field | Type | Description |
|---|---|---|
| accountNo Required | string | Customer bank account number (10-15 digits) |
| accName Required | string | Customer account name (2-100 characters) |
| bankCode Required | string | Bank code (e.g. KBANK, SCB, BBL) |
| amount Required | number | Amount in THB — integer baht only; any decimal portion is silently floored (e.g. 100.99 → 100). Range 1 - 2,000,000. |
| callbackUrl Required | string | HTTPS URL for status callbacks |
| refCode Optional | string | Client-side reference code (not included in signature, up to 100 chars) |
| signature Required | string | HMAC-SHA256 signature over the sorted query string (see Signature) |
| timestamp Required | number | Unix timestamp in milliseconds (must be within 5 minutes) |
Response Fields
| Field | Type | Description |
|---|---|---|
| txnId | string | Transaction ID (format: DEP{timestamp}{random}) |
| status | string |
Transaction status
pending
paid
expired
cancelled
|
| channel | string | Payment channel: QR (PromptPay), TRANSFER (bank transfer) or P2P (matched with another user). Always branch on this field — do not assume qrcode is present |
| amount | number | The EXACT amount the customer must transfer, satang included. On some accounts (e.g. GSB) the system adds satang to identify the transaction — a rounded transfer will not match. Always display this value, never requestedAmount |
| requestedAmount | number | The amount you asked for, before satang allocation. For your own reconciliation only — never show it as the amount to transfer |
| fee | number | Transaction fee (from MDR Profile, calculated on the satang-inclusive amount) |
| feePercent | number | Fee percentage |
| netAmount | number | Net amount after fee (amount - fee) |
| expireDate | string | Expiration date/time (ISO 8601, UTC) |
| refcode | string | Client reference code echoed back verbatim (empty string "" if not sent) |
| qrcode | string | PromptPay EMV payload for client-side QR rendering (only when channel=QR) |
| bankData | object | Bank account for transfer: bankName, bankCode, accountName, accountNumber (only when channel=TRANSFER) |
| url | string | The page to hand your customer. Present on every channel: on P2P it is the matching page that shows the account to transfer to, and on QR and TRANSFER it is the page where the customer attaches their transfer slip. Whether they MUST open it depends on the channel — see the table below. |
P2P: paying the wrong amount
A P2P deposit is matched to a withdrawal as one whole unit, and the customer is told to transfer that exact figure. The check on settlement compares the amount on the bank slip with the amount of the match, so anything other than an exact transfer has consequences worth building for.
| Customer transfers | What happens | Deposit credited |
|---|---|---|
| The exact amount | Settles normally. | Yes |
| Less than the amount | The deposit is held for review by our team. It is NOT treated as a part payment and it is NOT topped up by a second transfer — the deposit is finished. The withdrawal it was matched to is released and filled by other depositors, so the withdrawing side is unaffected. | No |
| More than the amount | Settles, but only the matched amount is credited. The excess is not returned automatically. | Matched amount only |
url: the page to hand your customer
Every deposit returns url — one field, on every channel. What it points at, and whether your customer has to open it, depends on channel.
| channel | Must the customer open it? | Why |
|---|---|---|
P2P |
Yes | There is no qrcode and no bankData on this channel — the account to transfer to appears only on that page, so a customer who never opens it cannot pay at all. |
TRANSFER |
Yes | The customer attaches their transfer slip there. Send them the link after they transfer. |
QR |
Optional | The QR is the payment method, so the deposit completes without it. The page is there if you want the customer to attach a slip as well. |
curl -X POST '__BASE_URL__/api/v1/deposit/create' \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your_api_key' \
-H 'X-API-Secret: your_api_secret' \
-H 'X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
-d '{
"accountNo": "1234567890",
"accName": "John Doe",
"bankCode": "KBANK",
"amount": 1000.00,
"callbackUrl": "https://your-domain.com/callback",
"refCode": "ORDER-12345",
"signature": "abc123...",
"timestamp": 1709123456789
}'
{
"status": "success",
"message": "Create Success",
"data": {
"txnId": "DEP1709123456ABC123",
"status": "pending",
"channel": "QR",
"amount": 1000.00,
"fee": 15.00,
"feePercent": 1.5,
"netAmount": 985.00,
"expireDate": "2024-01-01T12:10:00Z",
"refcode": "ORDER-12345",
"customerData": {
"accountNo": "1234567890",
"accName": "John Doe",
"bankCode": "KBANK"
},
"qrcode": "00020101021229370016A000000677010111011300105568164630530376454041000802TH5910COMPANY CO6304ABCD"
}
}
{
"status": "success",
"message": "Create Success",
"data": {
"txnId": "DEP1709123456ABC123",
"status": "pending",
"channel": "TRANSFER",
"amount": 1000.37,
"requestedAmount": 1000.00,
"fee": 15.01,
"feePercent": 1.5,
"netAmount": 985.36,
"expireDate": "2024-01-01T12:10:00Z",
"refcode": "ORDER-12345",
"customerData": {
"accountNo": "1234567890",
"accName": "John Doe",
"bankCode": "KBANK"
},
"bankData": {
"bankName": "GSB",
"bankCode": "GSB",
"accountName": "Company ABC",
"accountNumber": "020123456789"
}
}
}
{
"status": "success",
"message": "Create Success",
"data": {
"txnId": "DEP1709123456ABC123",
"status": "pending",
"channel": "P2P",
"amount": 500.00,
"requestedAmount": 500.00,
"fee": 7.00,
"feePercent": 1.4,
"netAmount": 493.00,
"expireDate": "2024-01-01T12:10:00Z",
"refcode": "ORDER-12345",
"customerData": {
"accountNo": "1234567890",
"accName": "John Doe",
"bankCode": "KBANK"
},
"url": "https://p2p.example.com/deposit/9xQ2m4Kd?t=..."
}
}
Payout API
Create payout requests to transfer funds to customer bank accounts.
Request Fields
| Field | Type | Description |
|---|---|---|
| accountNo Required | string | Destination bank account number |
| accName Required | string | Destination account name (must match bank records) |
| bankCode Required | string | Destination bank code (e.g. KBANK, SCB, BBL) |
| amount Required | number | Amount in THB (must have sufficient balance). Range 1 - 2,000,000. |
| callbackUrl Required | string | HTTPS URL for status callbacks |
| refCode Optional | string | Client-side reference code (not included in signature, up to 100 chars) |
| signature Required | string | HMAC-SHA256 signature over the sorted query string (see Signature) |
| timestamp Required | number | Unix timestamp in milliseconds (must be within 5 minutes) |
Response Fields
| Field | Type | Description |
|---|---|---|
| txnId | string | Transaction ID (format: PAY{timestamp}{random}) |
| status | string |
Transaction status
pending
processing
completed
failed
cancelled
|
| amount | number | Payout amount |
| fee | number | Withdrawal fee (from MDR Profile) |
| feePercent | number | Fee percentage |
| channel | string | Present only when the payout is fulfilled over the P2P channel, with the value "P2P". Absent on the ordinary bank-transfer channel |
| refcode | string | Client reference code echoed back verbatim (empty string "" if not sent) |
| customerData | object | Destination account details: accountNo, accName, bankCode |
| url | string | Status page for the withdrawing customer (only when channel=P2P). Optional — the payout completes without anyone opening it; it exists because on this channel there is no bank reference to show while the transfer is being matched |
P2P: a withdrawal delivered in two parts
A P2P withdrawal is filled by matching it against deposits. If not enough deposits arrive before the deadline, the part already delivered stays delivered and a system bank transfers the remainder to the same customer. Your customer receives the full amount either way; what changes is when.
| Situation | What happens | Callback |
|---|---|---|
| Filled by deposits | Settles normally. | payout.success |
| Filled in part, bank pays the rest | The remainder is transferred from a system bank to the same account. The customer ends up with the full amount, usually within minutes. | payout.success |
| Filled in part while the banks are closed | The banks that carry withdrawals close overnight. Rather than hold your withdrawal until they reopen, the unpaid remainder is returned to your wallet and the withdrawal is closed. Your customer keeps what was already sent. | payout.success with the amounts |
| Bank transfer for the remainder fails | The withdrawal is held by our team until the remainder is transferred. It is not cancelled and the amount already sent is never refunded, because it cannot be recalled and your customer asked for the whole amount. | None until it resolves |
curl -X POST '__BASE_URL__/api/v1/payout/create' \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your_api_key' \
-H 'X-API-Secret: your_api_secret' \
-H 'X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
-d '{
"accountNo": "9876543210",
"accName": "Jane Doe",
"bankCode": "SCB",
"amount": 5000.00,
"callbackUrl": "https://your-domain.com/callback",
"refCode": "WD-98765",
"signature": "def456...",
"timestamp": 1709123456789
}'
{
"status": "success",
"message": "Create Success",
"data": {
"txnId": "PAY1709123456XYZ789",
"amount": 5000.00,
"fee": 75.00,
"feePercent": 1.5,
"status": "pending",
"refcode": "WD-98765",
"customerData": {
"accountNo": "9876543210",
"accName": "Jane Doe",
"bankCode": "SCB"
}
}
}
{
"status": "success",
"message": "Create Success",
"data": {
"txnId": "PAY1709123456XYZ789",
"amount": 500.00,
"fee": 1.00,
"feePercent": 0.2,
"status": "pending",
"channel": "P2P",
"refcode": "WD-98765",
"customerData": {
"accountNo": "9876543210",
"accName": "Jane Doe",
"bankCode": "SCB"
},
"url": "https://p2p.example.com/withdraw/7bT1n8Le?t=..."
}
}
Balance API
Check your wallet balance including available and frozen amounts.
Response Fields
| Field | Type | Description |
|---|---|---|
| clientId | string | Client ID |
| name | string | Client name |
| balance | number | Total balance |
| available | number | Available balance (balance - frozen) |
| frozen | number | Frozen/locked amount |
| updatedAt | string | Last updated timestamp (Asia/Bangkok) |
curl -X GET '__BASE_URL__/api/v1/client/balance' \
-H 'X-API-Key: your_api_key' \
-H 'X-API-Secret: your_api_secret'
{
"status": "success",
"data": {
"clientId": "65a1b2c3d4e5f6a7b8c9d0e1",
"name": "ACME-01",
"balance": 150000.00,
"available": 148500.00,
"frozen": 1500.00,
"updatedAt": "2024-01-01T19:05:00+07:00"
}
}
Callback / Webhook
The system sends a POST request to your callback URL whenever a transaction status changes.
{
"event": "deposit.paid",
"txnId": "DEP1709123456ABC123",
"amount": 1000.00,
"status": "PAID",
"paidAt": "2024-01-01T12:05:00Z",
"signature": "callback_signature_here",
"timestamp": 1709123456789
}
Payout Callback Events: payout.success, payout.failed, payout.cancelled
// Payout Success
{
"event": "payout.success",
"txnId": "PAY1709123456XYZ789",
"amount": 5000.00,
"fee": 15.00,
"status": "SUCCESS",
"completedAt": "2024-01-01T14:30:00Z",
"signature": "callback_signature_here",
"timestamp": 1709123456789
}
// Payout Failed
{
"event": "payout.failed",
"txnId": "PAY1709123456XYZ789",
"amount": 5000.00,
"fee": 15.00,
"status": "FAILED",
"signature": "callback_signature_here",
"timestamp": 1709123456789
}
// Payout Cancelled
{
"event": "payout.cancelled",
"txnId": "PAY1709123456XYZ789",
"amount": 5000.00,
"fee": 15.00,
"status": "CANCELLED",
"signature": "callback_signature_here",
"timestamp": 1709123456789
}
Always verify the callback signature by re-generating the HMAC-SHA256 hash from the callback data using your API Secret. This ensures the callback is genuinely from our system.
Signature Generation
Generate the signature with HMAC-SHA256 over a sorted query string of exactly these 6 fields.
accNameaccountNoamountbankCodecallbackUrltimestamp
- refCode (optional client reference)
- signature (this is the output)
- timeout (optional)
- apiKey / apiSecret (HTTP headers only, never in signature)
Steps
- Pick the 6 fields listed above — do NOT include refCode/signature/timeout.
- Sort field names alphabetically A-Z (case-sensitive).
- Build the string: key1=value1&key2=value2&...
- Use raw values (no URL-encoding required — server accepts both).
- Compute HMAC-SHA256 using API Secret as the key.
- Output as a lowercase hex string (64 chars).
Example (Node.js)
const crypto = require('crypto');
// Allowed fields for signature — IMPORTANT: must be exactly these 6.
// Do NOT include refCode, signature, timeout, apiKey, or any other field.
const SIG_FIELDS = ['accName', 'accountNo', 'amount', 'bankCode', 'callbackUrl', 'timestamp'];
function generateSignature(data, apiSecret) {
// 1. Pick only the allowed fields + sort alphabetically (case-sensitive)
const sortedKeys = SIG_FIELDS.slice().sort();
// 2. Build query string: key=value joined by '&', raw values (no URL encoding)
const queryString = sortedKeys
.map(key => `${key}=${data[key]}`)
.join('&');
// 3. HMAC-SHA256 with API Secret as key, output as lowercase hex
return crypto
.createHmac('sha256', apiSecret)
.update(queryString)
.digest('hex');
}
// Usage (Deposit example)
const data = {
accName: 'John Doe',
accountNo: '1234567890',
bankCode: 'KBANK',
amount: 1000,
callbackUrl: 'https://your-site.com/callback',
refCode: 'ORDER-123', // stays in body but NOT in signature
timestamp: Date.now(), // Unix epoch ms, generate right before signing
};
data.signature = generateSignature(data, 'your_api_secret');
// data is now ready to POST as JSON
If your code produces the expected signature, your integration is ready.
// Test inputs (use these exact values in your code)
const apiSecret = 'test_secret_for_docs';
const data = {
accName: 'John Doe',
accountNo: '1234567890',
amount: 1000,
bankCode: 'KBANK',
callbackUrl: 'https://your-site.com/callback',
timestamp: 1709123456789,
};
// Step 2 output (sorted query string):
// accName=John Doe&accountNo=1234567890&amount=1000&bankCode=KBANK&callbackUrl=https://your-site.com/callback×tamp=1709123456789
// Expected HMAC-SHA256 output (lowercase hex):
// 7fcd73b8eb6a5e2f5250ab0011caa230171b11ca6af65b0954567f3dc9e78e25
const sig = generateSignature(data, apiSecret);
console.log(sig);
// if your code prints the same value the server computes, your integration is ready.
Banks API
Retrieve the list of supported banks with pagination and search (uses API Key like Deposit/Payout).
Query Parameters
| Field | Type | Description |
|---|---|---|
| page Optional | number | Page number (default: 1) |
| limit Optional | number | Items per page (default: 100) |
| search Optional | string | Search by bank name, bank code, or international code |
Response Fields
| Field | Type | Description |
|---|---|---|
| id | string | Bank ID (MongoDB ObjectId) |
| bank_name | string | Bank name |
| bank_code | string | Bank code (e.g. KBANK, SCB) |
| int_code | string | International code (e.g. 004, 014) |
| bank_logo | string | Bank logo URL |
curl -X GET '__BASE_URL__/api/v1/client/banks?page=1&limit=100' \
-H 'X-API-Key: your_api_key' \
-H 'X-API-Secret: your_api_secret'
Other Endpoints
Additional endpoints for managing deposits and payouts.
/api/v1/transaction/:txnId
Check transaction status (deposit or payout, by a single txnId)
Normalized status poll — resolves the txnId across both deposits and payouts and returns a lean, type-agnostic status view. Requires API Key. For full type-specific detail use /api/v1/deposit/:txnId or /api/v1/payout/:txnId.
curl -X GET '__BASE_URL__/api/v1/transaction/DEP1709123456ABC123' \
-H 'X-API-Key: your_api_key' \
-H 'X-API-Secret: your_api_secret'
Response
{
"status": "success",
"message": "Get Success",
"data": {
"txnId": "DEP1709123456ABC123",
"type": "deposit",
"status": "pending",
"amount": 1000.00,
"netAmount": 985.00,
"refcode": "ORDER-12345",
"createdAt": "2024-01-01T12:00:00Z"
}
}
Deposit Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/deposit/:requestId |
Get deposit details by ID |
| GET | /api/v1/deposit/status/:requestId |
Get deposit status (public, no auth required) |
| GET | /api/v1/deposits |
List all deposits with pagination |
| POST | /api/v1/deposit/:requestId/cancel |
Cancel a pending deposit |
/api/v1/deposit/:txnId/cancel
Cancel a pending deposit. Only deposits with pending status can be cancelled.
curl -X POST '__BASE_URL__/api/v1/deposit/DEP1709123456ABC123/cancel' \
-H 'X-API-Key: your_api_key' \
-H 'X-API-Secret: your_api_secret'
Response
{
"status": "success",
"message": "Deposit cancelled successfully"
}
Payout Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/payout/:requestId |
Get payout details by ID |
| GET | /api/v1/payout/status/:requestId |
Get payout status (public, no auth required) |
| GET | /api/v1/payouts |
List all payouts with pagination |
Bank Codes
Supported bank codes and their logos.
https://white-lable.sgp1.cdn.digitaloceanspaces.com/banks-logos/{CODE}.png


















