1. Authentication
All partner API requests sent to the P2P gateway must include your merchant specific API Key in the HTTP headers. Keep this key secure and do not share it.
X-API-Key: key-lucky-games
2. Flow A: Deposit Request (Cash-In)
Creates a deposit order. The gateway immediately locks the requested amount of gas from your gas_balance to cover potential netting fallbacks.
POST
http://localhost:3005/api/v1/deposit
{
"merchantUserId": "user_a1",
"amount": "100.0000",
"idempotencyKey": "ik_1781294819"
}
{
"id": 142,
"merchantId": 1,
"userId": 1,
"type": "DEPOSIT",
"amount": "100.0000",
"status": "PENDING_MATCH",
"idempotencyKey": "ik_1781294819",
"createdAt": "2026-06-13T10:00:00.000Z"
}
3. Flow B: Withdraw Request (Cash-Out)
Creates a withdrawal order. No gas is reserved. Withdrawals are queued and matched against deposit orders of the same amount.
POST
http://localhost:3005/api/v1/withdraw
{
"merchantUserId": "user_b1",
"amount": "100.0000",
"idempotencyKey": "ik_901847192"
}
4. P2P Escrow Action Signalling
Once orders are matched, customers interact directly to transfer cash. Partners signal transaction status updates to the gateway.
A. Mark Transferred (Paid)
The depositor signals that cash has been transferred to the withdrawer's bank account.
POST
/api/transaction/:id/transferred
B. Confirm & Release
The withdrawer verifies receipt of the funds and releases the escrow credit balance.
POST
/api/transaction/:id/release
C. File Dispute
If funds do not arrive, the withdrawer opens a dispute, pausing timeout expiration.
POST
/api/transaction/:id/dispute
{
"reason": "Customer uploaded fake slip"
}
5. Webhook Callback Event Structure
The gateway posts webhook updates to your registered webhookUrl when transaction states transition.
{
"event": "transaction.updated",
"timestamp": "2026-06-13T10:15:00.000Z",
"data": {
"txId": 142,
"merchantId": 1,
"type": "DEPOSIT",
"amount": "100.0000",
"status": "COMPLETED",
"idempotencyKey": "ik_1781294819"
}
}
5.1 Webhook Signature Verification (การตรวจสอบความถูกต้องลายเซ็น)
เพื่อความปลอดภัยสูงสุด ร้านค้าควรอ่านและตรวจสอบลายเซ็นที่ส่งมาใน Header X-Webhook-Signature ทุกครั้ง เพื่อป้องกันการปลอมแปลงคำขอโอนเงินจากบุคคลภายนอก ลายเซ็นนี้คำนวณจาก **HMAC-SHA256** โดยใช้ raw body ของ webhook payload ร่วมกับ Webhook Secret ของร้านค้า
Node.js (Express)
const crypto = require('crypto');
app.post('/api/callback', (req, res) => {
const signatureHeader = req.headers['x-webhook-signature'];
const webhookSecret = 'your_whsec_secret_here '; // ดูได้ที่หน้า Settings
// คำนวณ Signature จาก Raw Body
const rawBody = JSON.stringify(req.body);
const calculatedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody)
.digest('hex');
if (calculatedSignature === signatureHeader) {
console.log('Webhook signature valid!');
return res.status(200).send('success');
} else {
console.error('Invalid signature!');
return res.status(401).send('Unauthorized');
}
});
PHP
<?php
$signatureHeader = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$webhookSecret = 'your_whsec_secret_here ';
// รับ Raw Request Body
$rawBody = file_get_contents('php://input');
$calculatedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
if (hash_equals($calculatedSignature, $signatureHeader)) {
echo "Webhook signature valid!";
http_response_code(200);
} else {
echo "Invalid signature!";
http_response_code(401);
}
?>
Python (Flask)
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/api/callback', methods=['POST'])
def webhook_callback():
signature_header = request.headers.get('X-Webhook-Signature')
webhook_secret = b'your_whsec_secret_here '
# คำนวณ Signature จาก Raw Data
raw_body = request.get_data()
calculated_signature = hmac.new(
webhook_secret,
raw_body,
hashlib.sha256
).hexdigest()
if hmac.compare_digest(calculated_signature, signature_header):
print("Webhook signature valid!")
return "success", 200
else:
print("Invalid signature!")
abort(401)
5.2 Webhook Payload Decryption (การถอดรหัสข้อมูล Webhook)
หากร้านค้าเปิดใช้งาน Webhook Payload Encryption ในหน้าการตั้งค่า ข้อมูลดิบ (raw body) ของ Webhook จะถูกเข้ารหัสเพื่อความปลอดภัยระดับสูงโดยใช้ AES-256-GCM ข้อมูลจะจัดส่งมาในรูปแบบ JSON ดังนี้:
{
"ciphertext": "<hex_encoded_encrypted_data>",
"iv": "<hex_encoded_initialization_vector_12_bytes>",
"tag": "<hex_encoded_authentication_tag_16_bytes>"
}
ในการถอดรหัส ร้านค้าต้องใช้ Webhook Secret เพื่อนำมาหาค่าแฮช SHA-256 (32 ไบต์) สำหรับใช้เป็นคีย์สมมาตรหลักในการถอดรหัส AES-256-GCM:
Node.js
const crypto = require('crypto');
function decryptWebhookPayload(encryptedPayload, webhookSecret) {
const { ciphertext, iv, tag } = encryptedPayload;
// 1. Derive the 32-byte key via SHA-256 hash of the webhook secret
const key = crypto.createHash('sha256').update(webhookSecret).digest();
// 2. Setup decipher using AES-256-GCM
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(tag, 'hex'));
// 3. Decrypt the payload
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
PHP
<?php
function decryptWebhookPayload($encryptedPayload, $webhookSecret) {
$ciphertext = hex2bin($encryptedPayload['ciphertext']);
$iv = hex2bin($encryptedPayload['iv']);
$tag = hex2bin($encryptedPayload['tag']);
// 1. Derive key via SHA-256 hash of secret
$key = hash('sha256', $webhookSecret, true);
// 2. Decrypt using openssl
$decrypted = openssl_decrypt(
$ciphertext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($decrypted === false) {
throw new Exception('Decryption failed');
}
return json_decode($decrypted, true);
}
?>
Python (cryptography)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import hashlib
import json
def decrypt_webhook_payload(encrypted_payload, webhook_secret):
ciphertext = bytes.fromhex(encrypted_payload['ciphertext'])
iv = bytes.fromhex(encrypted_payload['iv'])
tag = bytes.fromhex(encrypted_payload['tag'])
# 1. Derive key via SHA-256 hash of secret
key = hashlib.sha256(webhook_secret.encode('utf-8')).digest()
# 2. Decrypt using AESGCM (cryptography expects ciphertext and tag concatenated)
aesgcm = AESGCM(key)
decrypted_bytes = aesgcm.decrypt(iv, ciphertext + tag, None)
return json.loads(decrypted_bytes.decode('utf-8'))
6. คู่มือการเชื่อมต่อ API (API Integration Guide)
คำแนะนำทีละขั้นตอนสำหรับนักพัฒนาในการเชื่อมต่อระบบร้านค้า (Merchant System) เข้ากับเกตเวย์ P2P (Cross-Merchant P2P Gateway)
ขั้นตอนการฝากเงิน (Flow A: Deposit - Cash-In)
1
สร้างคำขอฝากเงิน (Create Order)
ส่ง HTTP POST ไปที่ POST /api/v1/deposit โดยระบบจะตรวจสอบ Gas Balance ของร้านค้า จากนั้นทำการสำรอง (Reserve) Gas ในอัตรา 1:1 กับยอดฝาก เพื่อการันตีธุรกรรม และปรับสถานะเป็น PENDING_MATCH
2
รอจับคู่และรับพิกัดบัญชีปลายทาง (Order Matched)
เมื่อระบบจับคู่กับผู้ถอนเงิน (Withdrawer) ได้สำเร็จ สถานะจะเปลี่ยนเป็น WAITING_PAYMENT และส่ง Webhook ไปยัง webhookUrl ของร้านค้า โดยจะมีรายละเอียดบัญชีธนาคารของผู้รับเงินปลายทางแนบไปด้วย
3
โอนเงินจริงและยืนยันการโอน (Transfer & Notify)
ผู้ฝากเงินโอนเงินจริงผ่าน Mobile Banking ไปยังบัญชีธนาคารที่ได้รับ เมื่อโอนเรียบร้อย ระบบร้านค้าต้องส่งคำขอแจ้งว่าโอนสำเร็จแล้วไปที่ POST /api/v1/transaction/:id/transferred เพื่อปรับสถานะธุรกรรมเป็น PAID_UNCONFIRMED
4
คู่ค้าปล่อยเครดิตและเสร็จสิ้นธุรกรรม (Release & Settle)
เมื่อฝั่งผู้ถอนเงินตรวจสอบยอดเงินเข้าบัญชีเรียบร้อยและกดยอมรับ รายการจะเปลี่ยนสถานะเป็น COMPLETED ค่า Gas ที่ล็อคไว้จะถูก Netting หักล้างและบันทึกดุลบัญชีอย่างเป็นทางการ
ขั้นตอนการถอนเงิน (Flow B: Withdraw - Cash-Out)
1
สร้างคำขอถอนเงิน (Create Withdraw)
ส่ง HTTP POST ไปที่ POST /api/v1/withdraw โดยระบบจะนำคำขอนี้เข้าสู่ Matching Engine เพื่อรอการจับคู่กับรายการฝาก
2
รอรับการแจ้งเตือนการโอนเงิน (Wait Payment)
เมื่อจับคู่สำเร็จและฝั่งผู้ฝากโอนเงินมาแล้ว สถานะจะปรับเปลี่ยนผ่าน Webhook เป็น PAID_UNCONFIRMED เพื่อส่งสัญญาณให้คุณไปตรวจสอบบัญชีธนาคารจริง
3
ตรวจสอบเงินเข้าและกดยืนยันปล่อยยอด (Verify & Release)
หากเงินเข้าบัญชีธนาคารจริงอย่างถูกต้อง ให้ส่งคำขอไปที่ POST /api/transaction/:id/release เพื่อส่งเครดิตและจบธุรกรรม (สถานะ COMPLETED)
4
แจ้งเปิดข้อพิพาทเมื่อเกิดปัญหา (Dispute Override)
หากเลยกำหนดเวลา (Timeout) หรือยอดเงินไม่เข้าจริง หรือผู้ฝากแนบสลิปปลอม ให้ส่งคำขอ POST /api/transaction/:id/dispute ทันที เพื่อระงับเวลาถอยหลัง และส่งธุรกรรมเข้าสู่ Dispute Center ให้แอดมินพิจารณาตัดสิน
เปิดโหมด Sandbox ในตัวอย่างโค้ด (Add Header 'X-Sandbox: true')
ตัวอย่างการเขียนโปรแกรมเชื่อมต่อ (Code Examples)
const axios = require('axios');
async function createDeposit() {
try {
const response = await axios.post('http://localhost:3005/api/v1/deposit', {
merchantUserId: 'user_lucky_01',
amount: '100.00',
idempotencyKey: 'ik_' + Date.now()
}, {
headers: {
'X-API-Key': 'key-lucky-games ',
'Content-Type': 'application/json'
}
});
console.log('Order Created:', response.data);
} catch (error) {
console.error('API Error:', error.response ? error.response.data : error.message);
}
}
<?php
$ch = curl_init('http://localhost:3005/api/v1/deposit');
$payload = json_encode([
'merchantUserId' => 'user_lucky_01',
'amount' => '100.00',
'idempotencyKey' => 'ik_' . time()
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: key-lucky-games '
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
echo "Response: " . $result;
?>
import requests
import time
url = "http://localhost:3005/api/v1/deposit"
headers = {
"Content-Type": "application/json",
"X-API-Key": "key-lucky-games "
}
payload = {
"merchantUserId": "user_lucky_01",
"amount": "100.00",
"idempotencyKey": f"ik_{int(time.time())}"
}
response = requests.post(url, json=payload, headers=headers)
print("Response JSON:", response.json())
7. การหมุนเวียนคีย์โดยไม่หยุดชะงัก (Zero-Downtime API Key Rotation)
ในการหมุนเวียนคีย์ API (API Key Rotation) เกตเวย์จะเปิดใช้งานคีย์เดิมเป็น Transition Key อัตโนมัติเป็นเวลา 24 ชั่วโมงหลังจากสร้างคีย์ใหม่
ระบบหลังบ้านของร้านค้าสามารถส่งคำขอด้วยคีย์ใหม่ หรือใช้คีย์เดิมในช่วงเปลี่ยนผ่านได้โดยไม่มีช่วงระบบล่ม (Zero Downtime)
const axios = require('axios');
// เก็บคีย์ API ล่าสุด และคีย์สำรอง (Transition Key)
const apiClient = axios.create({
baseURL: 'http://localhost:3005',
timeout: 5000,
headers: { 'Content-Type': 'application/json' }
});
const API_KEYS = {
primary: 'key-rotated-8bf292cb ', // คีย์ใหม่ล่าสุด
transition: 'key-lucky-games ' // คีย์เก่า (ที่กำลังใช้งานในช่วงเปลี่ยนผ่าน)
};
// Middleware หรือ helper สำหรับยิง HTTP request แบบมี Failover
async function requestWithKeyRotation(endpoint, data) {
// 1. ลองยิงด้วยคีย์ Primary ก่อน
try {
const response = await apiClient.post(endpoint, data, {
headers: { 'X-API-Key': API_KEYS.primary }
});
console.log('Success with Primary Key!');
return response.data;
} catch (error) {
// 2. หากเฟลด้วยสิทธิ์ไม่ถูกต้อง (401 Unauthorized) ให้ลอง fallback ยิงด้วย Transition Key
if (error.response && error.response.status === 401 && API_KEYS.transition) {
console.warn('Primary Key unauthorized. Trying Transition API Key...');
try {
const response = await apiClient.post(endpoint, data, {
headers: { 'X-API-Key': API_KEYS.transition }
});
console.log('Success with Transition Key!');
return response.data;
} catch (fallbackError) {
throw fallbackError;
}
}
throw error;
}
}
// วิธีเรียกใช้งาน
// requestWithKeyRotation('/api/deposit', { ... });
8. การตรวจสอบลายเซ็น Webhook (Webhook Signature Verification)
ในการส่งข้อมูลการอัปเดตสถานะธุรกรรม (Webhook) เกตเวย์จะคำนวณค่าแฮช HMAC-SHA256 ของ Payload ดิบด้วย Webhook Secret ของร้านค้า และส่งมาใน Header X-Webhook-Signature
ร้านค้าต้องนำข้อมูลใน Body ทั้งหมดมาทำแฮชเปรียบเทียบในฝั่งเซิร์ฟเวอร์ปลายทางเพื่อยืนยันว่าข้อมูลส่งมาจากเกตเวย์จริงและไม่ได้ถูกปลอมแปลง (Signature Verification)
const express = require('express');
const crypto = require('crypto');
const app = express();
// หมายเหตุ: ต้องใช้ Raw Body เพื่อคำนวณลายเซ็นที่ถูกต้อง
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const webhookSecret = "your_whsec_secret_here "; // จะแทนที่ด้วยความลับจริงเมื่อเข้าระบบ
const computedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(req.body) // req.body ต้องเป็น Raw Buffer ที่ได้จาก express.raw
.digest('hex');
if (signature === computedSignature) {
console.log('Webhook signature valid!');
const payload = JSON.parse(req.body.toString());
console.log('Processed Event:', payload.event, 'Transaction ID:', payload.data.id);
res.sendStatus(200);
} else {
console.error('Webhook signature verification failed!');
res.sendStatus(400);
}
});