When a withdraw or settlement order completes (success or fail), we send an HTTP POST to the notify_url you provided. Both share the same payload shape — distinguish them by the mode marker (4th character) of platform_order_id:

เมื่อ withdraw หรือ settlement order เสร็จ (สำเร็จหรือล้มเหลว) ระบบเราจะ POST ไปที่ notify_url ของคุณ
ทั้งสองใช้ payload shape เดียวกัน
แยกกันที่ ตัว mode (ตัวที่ 4) ของ platform_order_id:

Origin platform_order_id mode marker mode มาจาก ตัว mode ของ platform_order_id mode
/withdraw/create W (e.g. ABCW20260508abc123XYZ456) "WITHDRAW" /withdraw/create W (เช่น ABCW20260508abc123XYZ456) "WITHDRAW"
/thb-settlement/create M (e.g. ABCM20260509abc123XYZ456) "WITHDRAW" /thb-settlement/create M (เช่น ABCM20260509abc123XYZ456) "WITHDRAW"

Format: 24 characters = <3-letter prefix> + P/W/M (mode marker) + YYYYMMDD + 12 random chars.

รูปแบบ: 24 ตัว = <prefix 3 ตัวอักษร> + P/W/M (ตัว mode) + YYYYMMDD + 12 ตัวสุ่ม

For delivery rules (retry policy, timeout, idempotency), see Webhooks Overview.

รายละเอียดการส่ง (retry, timeout, idempotency) อยู่ที่ Webhooks Overview

Payload

Payload

Example — withdraw success

ตัวอย่าง — withdraw สำเร็จ

JSON
{
  "merchant_id":       "AA12345678",
  "platform_order_id": "ABCW20260508abc123XYZ456",
  "merchant_order_id": "PAYOUT-2026-001",
  "mode":              "WITHDRAW",
  "bank":              "KBANK",
  "account_no":        "1234567890",
  "account_name":      "ลูกค้า ปลายทาง",
  "amount":            1000.00,
  "status":            "SUCCESS",
  "timestamp":         1746694842000
}

Example — settlement success

ตัวอย่าง — settlement สำเร็จ

JSON
{
  "merchant_id":       "AA12345678",
  "platform_order_id": "ABCM20260509abc123XYZ456",
  "merchant_order_id": "SETTLE-2026-001",
  "mode":              "WITHDRAW",
  "bank":              "KBANK",
  "account_no":        "1234567890",
  "account_name":      "บริษัท เมอร์แชนต์ จำกัด",
  "amount":            50000.00,
  "status":            "SUCCESS",
  "timestamp":         1746780318000
}

Example — failed payout

ตัวอย่าง — ถอนไม่สำเร็จ

JSON
{
  "merchant_id":       "AA12345678",
  "platform_order_id": "ABCW20260508abc123XYZ456",
  "merchant_order_id": "PAYOUT-2026-001",
  "mode":              "WITHDRAW",
  "bank":              "KBANK",
  "account_no":        "1234567890",
  "account_name":      "ลูกค้า ปลายทาง",
  "amount":            1000.00,
  "status":            "FAIL",
  "timestamp":         1746694842000
}

Field reference

รายละเอียดฟิลด์

FieldTypeDescription ฟิลด์ประเภทคำอธิบาย
merchant_idstring Your merchant identifier (echoed). merchant_idstring รหัส merchant (echo)
platform_order_idstring Order ID assigned by the gateway. Format: 24 chars = <3-letter prefix> + mode marker (W = withdraw, M = settlement) + YYYYMMDD + 12 random chars. Use as your unique key for deduplication. platform_order_idstring Order ID ที่ออกโดย gateway
รูปแบบ 24 ตัว = <prefix 3 ตัวอักษร> + ตัว mode (W = withdraw, M = settlement) + YYYYMMDD + 12 ตัวสุ่ม
ใช้เป็น unique key dedup
merchant_order_idstring Your merchant_order_id from the original create call. merchant_order_idstring merchant_order_id ที่คุณส่งตอนสร้าง
modeenum Always "WITHDRAW" — for both withdraw and settlement callbacks. modeenum เป็น "WITHDRAW" เสมอ — ทั้ง withdraw และ settlement
bank / account_no / account_namestring Echo of the destination bank info from the order. bank / account_no / account_namestring Echo ข้อมูลธนาคารปลายทางจาก order
amountnumber Payout amount (THB). Cross-check against your stored order. amountnumber ยอดถอน (THB) — เช็คกับ order ที่เก็บไว้
statusenum "SUCCESS" on success, "FAIL" on failure (bank rejection, insufficient funds, account closed). Terminal — will not change. statusenum "SUCCESS" ถ้าสำเร็จ
"FAIL" ถ้าไม่สำเร็จ (ธนาคารปฏิเสธ, ยอดไม่พอ, บัญชีปิด)
terminal เปลี่ยนไม่ได้
timestampnumber Unix epoch in milliseconds. timestampnumber Unix epoch หน่วย milliseconds
PAYMENT vs WITHDRAW status values differ
สถานะ PAYMENT vs WITHDRAW ไม่เหมือนกัน

Payment callbacks use PAID/FAIL while withdraw/settlement callbacks use SUCCESS/FAIL. Branch on mode first, then check status.

Payment callback ใช้ PAID/FAIL ส่วน withdraw/settlement ใช้ SUCCESS/FAIL
ให้แยก branch ตาม mode ก่อน แล้วค่อยเช็ค status

Verification — full handler example

ตัวอย่าง Handler ครบ

A reference handler that covers both withdraw and settlement callbacks. The signature verification logic is identical to the payment callback handler — see Webhooks Overview.

Handler ตัวอย่างที่รับทั้ง withdraw และ settlement callback
Logic การตรวจ signature เหมือน payment callback
ดู Webhooks Overview

# Send a fake withdraw callback to your handler for local testing
SECRET='YOUR_SECRET'
BODY='{"merchant_id":"AA12345678","platform_order_id":"ABCW20260508abc123XYZ456","merchant_order_id":"PAYOUT-2026-001","mode":"WITHDRAW","bank":"KBANK","account_no":"1234567890","account_name":"ลูกค้า ปลายทาง","amount":1000,"status":"SUCCESS","timestamp":1746694842000}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST https://<your-merchant-webhook-URL>/withdraw-callback \
  -H "User-Agent: <gateway-user-agent>" \
  -H "Content-Type: application/json" \
  -H "Connection: close" \
  -H "X-Signature: $SIG" \
  -d "$BODY"
// Express handler — withdraw / settlement callback receiver
import express from 'express';
import crypto from 'node:crypto';

const SECRET = process.env.GATEWAY_SECRET;
const app = express();

app.post('/withdraw-callback',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const rawBody  = req.body;
    const received = req.get('x-signature') || '';
    const expected = crypto.createHmac('sha256', SECRET)
                           .update(rawBody)
                           .digest('hex');

    // 1. Signature
    if (received.length !== expected.length ||
        !crypto.timingSafeEqual(
          Buffer.from(received, 'hex'),
          Buffer.from(expected, 'hex'))) {
      return res.status(401).send('bad signature');
    }

    const event = JSON.parse(rawBody.toString('utf8'));
    if (event.mode !== 'WITHDRAW') {
      return res.status(400).send('wrong mode');
    }

    // 2. Distinguish withdraw vs settlement by mode marker (4th char of platform_order_id)
    const modeMarker = event.platform_order_id[3];   // 'W' = withdraw, 'M' = settlement
    const kind       = modeMarker === 'M' ? 'settlement' : 'withdraw';

    // 3. Idempotency
    const inserted = await db.insertCallbackLog({
      platform_order_id: event.platform_order_id,
      status:            event.status,
      raw_body:          rawBody.toString('utf8'),
    });
    if (!inserted) return res.status(200).send('ok');

    // 4. Cross-check + update
    const order = await db.findOrder(kind, event.merchant_order_id);
    if (!order || Number(order.amount) !== Number(event.amount)) {
      return res.status(400).send('amount mismatch');
    }

    if (event.status === 'SUCCESS') {
      await db.markOrderSuccess(kind, order.id);
    } else {
      await db.markOrderFailed(kind, order.id);
    }

    res.status(200).send('ok');
  }
);
<?php
// withdraw-callback.php — handles both withdraw and settlement callbacks
$secret = getenv('GATEWAY_SECRET');

$rawBody  = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $rawBody, $secret);

// 1. Signature
if (!hash_equals($expected, $received)) {
    http_response_code(401);
    echo 'bad signature';
    exit;
}

$event = json_decode($rawBody, true);
if (($event['mode'] ?? null) !== 'WITHDRAW') {
    http_response_code(400);
    echo 'wrong mode';
    exit;
}

// 2. Distinguish withdraw vs settlement by mode marker (4th char of platform_order_id)
$modeMarker = $event['platform_order_id'][3] ?? '';   // 'W' = withdraw, 'M' = settlement
$kind       = $modeMarker === 'M' ? 'settlement' : 'withdraw';

// 3. Idempotency
$pdo = new PDO(/* ... */);
$stmt = $pdo->prepare(
    'INSERT IGNORE INTO callback_log
        (platform_order_id, status, raw_body, received_at)
     VALUES (?, ?, ?, NOW())'
);
$stmt->execute([
    $event['platform_order_id'],
    $event['status'],
    $rawBody,
]);
if ($stmt->rowCount() === 0) {
    http_response_code(200);
    echo 'ok';
    exit;
}

// 4. Cross-check + update
$table = $kind === 'settlement' ? 'settlements' : 'withdraws';
$lookup = $pdo->prepare("SELECT * FROM {$table} WHERE merchant_order_id = ?");
$lookup->execute([$event['merchant_order_id']]);
$row = $lookup->fetch();

if (!$row || (float)$row['amount'] !== (float)$event['amount']) {
    http_response_code(400);
    echo 'amount mismatch';
    exit;
}

$newStatus = $event['status'] === 'SUCCESS' ? 'success' : 'failed';
$pdo->prepare("UPDATE {$table} SET status = ? WHERE id = ?")
    ->execute([$newStatus, $row['id']]);

http_response_code(200);
echo 'ok';

Quick checklist

Checklist สรุป

  • ✅ Read the raw body before any JSON parsing
  • ✅ อ่าน raw body ก่อน parse JSON
  • ✅ Verify X-Signature with timing-safe comparison
  • ✅ ตรวจ X-Signature ด้วย timing-safe comparison
  • ✅ Reject if mode is not "WITHDRAW"
  • ✅ ปฏิเสธถ้า mode ไม่ใช่ "WITHDRAW"
  • ✅ Distinguish withdraw vs settlement by the mode marker (4th char W/M) of platform_order_id — they live in different tables on your side
  • ✅ แยก withdraw vs settlement ด้วยตัว mode (ตัวที่ 4 W/M) ของ platform_order_id — โดยปกติจะอยู่คนละ table
  • ✅ Use platform_order_id as the unique key for dedup
  • ✅ ใช้ platform_order_id เป็น unique key dedup
  • ✅ Cross-check amount against your stored order
  • ✅ เช็ค amount เทียบ order
  • ✅ Return HTTP 200 only after your DB transaction commits
  • ✅ ตอบ HTTP 200 หลัง DB transaction commit แล้ว