Skip to content

สร้างรายการถอน ​

ขอบเขตปัจจุบัน — implementation 0092 รอตรวจและ deploy

เริ่มจาก Live P2P API ที่พัฒนาแล้ว ผู้ให้บริการต้องยืนยันรุ่นที่ deploy และการตั้งค่าของคุณก่อนใช้งานเงินจริง ตัวอย่างในหัวข้อ Target contract ด้านล่างยังไม่ใช่สิ่งที่รุ่นนี้เปิดครบ

คำขอใน implementation 0092 ​

ใช้ POST /v1/pay/withdrawals กับ Live key ที่ได้รับ withdrawals:write และ HMAC เดิม ต้องมี Idempotency-Key 8–200 ตัวอักษร เมื่อ retry ใช้ body/key เดิม แต่ nonce และลายเซ็นใหม่

json
{
  "amount_minor": 100000,
  "currency": "THB",
  "merchant_reference": "WD-20261017-0001",
  "end_user_ref": "customer-001",
  "beneficiary_id": "ben_92000000000040008000000000000423",
  "metadata": { "order": "WD-20261017-0001" }
}

amount_minor เป็นจำนวนเต็มบวกหน่วยสตางค์ ไม่เกิน 9007199254740991 และต้องผ่านยอดขั้นต่ำตามนโยบาย P2P ปัจจุบัน reference ทั้งสองยาว 1–64 ตัวอักษร ไม่มี control characters; merchant_reference ไม่ซ้ำต่อ Merchant metadata รับ string ไม่เกิน 20 keys ชื่อ key ยาว 1–40 ตัวอักษร ค่าไม่เกิน 500 ตัวอักษร ข้อมูล client ที่รองรับจะตรวจรูปแบบแล้วไม่เก็บใน metadata/response

ใช้เฉพาะ beneficiary ที่มีอยู่และยืนยันแล้ว รูปแบบ ben_ + UUID hex พิมพ์เล็ก 32 ตัว ของ Merchant/Live mode เดียวกัน รุ่นนี้ไม่รับ inline beneficiary และยังไม่มี Customer/beneficiary registration API หรือ Merchant quote ในขอบเขตนี้

ผลตอบกลับความหมายในรุ่นนี้
201pending_approval, requires_approval: true, version: 0 กัน principal + fee แล้ว และรอพนักงานอนุมัติสองคนก่อน matching
402 insufficient_balanceavailable ไม่พอสำหรับ principal + fee ไม่สร้างรายการ
409 beneficiary_not_verifiedบัญชีไม่พร้อมหรือไม่ใช่ของ Merchant/Live mode นี้
422 amount_below_p2p_minimumจำนวนต่ำกว่านโยบาย P2P ที่ใช้อยู่

ตรึงราคาขาย Payout ทั้งสี่ source ตอนสร้าง โดยฐาน fee ปัจจุบันใช้ wl_pool อ่าน fee_total_minor และ total_locked_minor จาก response 201 ยังไม่ใช่เงินถึงผู้รับ ให้ติดตาม GET /v1/pay/withdrawals/{id} ด้วย withdrawals:read จน status: "settled" และ remaining_minor: 0 event รุ่นนี้เป็น business snapshot แบบเดิม ไม่ใช่ Withdrawal เต็มใบ ดู endpoint, pagination และ event ที่รองรับ

Target contract — flow ขยายที่ implementation 0092 ยังไม่เปิดครบ

ตัวอย่างส่วนนี้เป็นเป้าหมายการเชื่อมต่อ

inline beneficiary, การลงทะเบียนบัญชีผ่าน Public API, quote และชุด event เต็มด้านล่าง ต้องอ้างอิง ขอบเขตที่พัฒนาแล้ว ก่อนนำไปใช้

รายการถอนคือการสั่งให้ระบบหา ผู้ฝาก มาโอนเงินเข้าบัญชีปลายทางที่คุณระบุ เงินไม่ได้ออกจากบัญชีกลางของเรา — ผู้ฝากโอนตรงเข้าบัญชีนั้นทีละก้อน

สองอย่างที่ต้องมีก่อน มิฉะนั้นสร้างไม่ได้เลย:

ต้องมีไม่มีแล้วเกิดอะไร
บัญชีปลายทางที่ status = 'verified' แล้ว409 beneficiary_not_verified · ไม่มี order ถูกสร้าง ไม่มีการล็อกเงิน
available_minor ≥ ยอดถอน + ค่าธรรมเนียม402 insufficient_balance · ไม่มี order ถูกสร้าง

ค่าธรรมเนียมของเรา "บวกเพิ่ม" ไม่ใช่ "หักจากยอดถอน"

gateway หลายเจ้าในตลาดหักค่าธรรมเนียมออกจากยอดถอน — ผู้ใช้สั่งถอน 1,000 ได้รับจริง 973.25

ของเราไม่ใช่แบบนั้น ผู้ใช้สั่งถอน 1,000 ก็ได้รับ 1,000 เต็ม แล้วค่าธรรมเนียมถูก บวกเพิ่ม หักจากกระเป๋าของคุณต่างหาก ⇒ ยอดที่ถูกล็อกจริงคือ total_locked_minor = amount_total_minor + fee_total_minor

สิ่งที่คุณต้องทำเพิ่ม: เช็กยอดในระบบของคุณด้วยตัวเลข ยอดถอน + ค่าธรรมเนียม ตั้งแต่หน้าจอ ไม่ใช่แค่ยอดถอน · เรียก /v1/pay/quote ก่อนเปิดฟอร์มเสมอ

Endpoint ​

POST /v1/pay/withdrawals

Header ที่บังคับ ​

headerค่า
AuthorizationBearer sk_live_...
PG-Signaturev1=<hex ของ HMAC-SHA256> — ดู การยืนยันตัวตนของ request
PG-Timestampunix seconds (คลาดเคลื่อนได้ ±300 วิ)
PG-Nonceไม่ซ้ำภายใน 10 นาที · เปลี่ยนทุก attempt
Idempotency-Keyบังคับทุก POST · TTL ฝั่งเรา 7 วัน
PG-Versionเช่น 2026-08-01

Idempotency-Key ต้องเขียนลง DB ก่อนยิง ไม่ใช่ gen ตอน retry

ถ้า retry แล้ว gen key ใหม่ คุณจะได้ รายการถอนสองใบ และเงินถูกล็อกสองเท่า · retry ทุกครั้งต้องอ่าน key เดิมจาก DB แล้วยิงซ้ำด้วย key เดิม (เปลี่ยนแค่ PG-Nonce)

Idempotency-Key อยู่ใน canonical string ของลายเซ็นด้วย — เปลี่ยน key แล้วไม่เซ็นใหม่ = 401

Request body ​

fieldบังคับชนิดหมายเหตุ
amount_minor✅int > 0🔴 หน่วยสตางค์ · 1,000.00 บาท = 100000 · ในตัว object ที่ตอบกลับมา field นี้ชื่อ amount_total_minor
currency✅stringปัจจุบัน "THB" เท่านั้น
merchant_reference✅string ≤ 64unique ต่อ merchant
end_user_ref✅string ≤ 64id ผู้ใช้ในระบบคุณ
beneficiary_id✅ (หรือ beneficiary{})stringได้จาก POST /v1/pay/beneficiaries · ต้อง verified แล้ว
beneficiary✅ (หรือ beneficiary_id)object{bank_code, account_number, account_name_th, promptpay_id?} · ถ้าบัญชีนี้ยังไม่ผ่าน verify → 409 beneficiary_not_verified
client—object{device_fingerprint, ip, user_agent}
metadata—object≤ 20 keys · ≤ 500 bytes ต่อค่า

ใช้ beneficiary_id ไม่ใช่ beneficiary{}

beneficiary{} แบบกรอกสดผ่านได้ก็ต่อเมื่อบัญชีนั้น verify ผ่านมาก่อนแล้ว ⇒ ถ้าคุณส่งแบบกรอกสดโดยไม่เคย verify คุณจะได้ 409 ทุกครั้ง

flow ที่ถูกคือ: สร้าง beneficiary → รอ beneficiary.verified → ค่อยสร้างรายการถอนด้วย beneficiary_id

ตัวอย่างเต็ม ​

ถอน 5,000.00 บาท (500000) ค่าธรรมเนียม 90.95 บาท (9095) ⇒ ต้องมี available_minor ≥ 509095

curl ​

bash
curl -sS -X POST 'https://api.example-wl.com/v1/pay/withdrawals' \
  -H 'Authorization: Bearer sk_live_9f2c1ab6e0d74f' \
  -H 'PG-Timestamp: 1755745331' \
  -H 'PG-Nonce: 01K3PA5T8V0X2Z4B6D8F0H2J4L' \
  -H 'PG-Version: 2026-08-01' \
  -H 'Idempotency-Key: wdr-u88213-20260821-0001' \
  -H 'PG-Signature: v1=6b1f0c9d8e7a4b2c5d3e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c' \
  -H 'Content-Type: application/json' \
  -d '{
    "amount_minor": 500000,
    "currency": "THB",
    "merchant_reference": "WD-20260821-0001",
    "end_user_ref": "u_88213",
    "beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC",
    "client": {
      "device_fingerprint": "df_9a1c4e77b2",
      "ip": "203.0.113.44",
      "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X)"
    },
    "metadata": { "user_id": "u_88213", "ticket": "WD-20260821-0001" }
  }'

Node (fetch) ​

js
// pgFetch() = helper ที่เซ็น canonical string ให้ ดู /get-started/authentication
import { pgFetch } from './pg-client.js'
import { db } from './db.js'

export async function createWithdrawal({ userId, amountMinor, beneficiaryId, ref }) {
  // 🔴 เขียน Idempotency-Key ลง DB ก่อนยิง — retry ทุกครั้งอ่านค่าเดิมกลับมาใช้
  const key = await db.oneValue(
    `INSERT INTO wd_requests (merchant_reference, idem_key)
     VALUES ($1, $2) ON CONFLICT (merchant_reference) DO UPDATE SET merchant_reference = EXCLUDED.merchant_reference
     RETURNING idem_key`,
    [ref, `wdr-${userId}-${ref}`]
  )

  const res = await pgFetch('POST', '/v1/pay/withdrawals', {
    idempotencyKey: key,
    body: {
      amount_minor: amountMinor,
      currency: 'THB',
      merchant_reference: ref,
      end_user_ref: userId,
      beneficiary_id: beneficiaryId,
      client: { device_fingerprint: 'df_9a1c4e77b2', ip: '203.0.113.44', user_agent: 'Mozilla/5.0 ...' },
      metadata: { user_id: userId, ticket: ref },
    },
  })

  if (res.status === 402) {
    const d = res.body.error.details
    // 🔴 ไม่มี order ถูกสร้าง → ปลดล็อกยอดในระบบคุณคืนผู้ใช้ "ทันที"
    await unlockUserBalance(userId, ref)
    throw new NotEnoughBalance(d.available_minor, d.required_minor, d.pending_clearing_available_at)
  }
  if (res.status === 409 && res.body.error.code === 'beneficiary_not_verified') {
    await unlockUserBalance(userId, ref)
    throw new BeneficiaryNotReady()
  }
  if (res.status !== 201) throw new PgError(res.body.error)

  const w = res.body
  await db.none(
    `INSERT INTO pg_withdrawals (pg_withdrawal_id, pg_version, status,
                                 amount_total_minor, fee_total_minor, total_locked_minor,
                                 filled_minor, remaining_minor, our_ref)
     VALUES ($1,$2,$3,$4,$5,$6,0,$4,$7)
     ON CONFLICT (pg_withdrawal_id) DO NOTHING`,
    [w.id, w.version, w.status, w.amount_total_minor, w.fee_total_minor, w.total_locked_minor, ref]
  )
  return w
}

PHP ​

php
<?php
// pg_request() = helper ที่เซ็น canonical string ให้ ดู /get-started/authentication
function create_withdrawal(PDO $db, string $userId, int $amountMinor, string $beneficiaryId, string $ref): array
{
    // 🔴 เขียน Idempotency-Key ลง DB ก่อนยิง
    $st = $db->prepare(
        'INSERT INTO wd_requests (merchant_reference, idem_key) VALUES (:r, :k)
         ON CONFLICT (merchant_reference) DO UPDATE SET merchant_reference = EXCLUDED.merchant_reference
         RETURNING idem_key'
    );
    $st->execute([':r' => $ref, ':k' => "wdr-{$userId}-{$ref}"]);
    $idemKey = $st->fetchColumn();

    [$status, $body] = pg_request('POST', '/v1/pay/withdrawals', [
        'amount_minor'       => $amountMinor,
        'currency'           => 'THB',
        'merchant_reference' => $ref,
        'end_user_ref'       => $userId,
        'beneficiary_id'     => $beneficiaryId,
        'client'             => [
            'device_fingerprint' => 'df_9a1c4e77b2',
            'ip'                 => '203.0.113.44',
            'user_agent'         => 'Mozilla/5.0 ...',
        ],
        'metadata'           => ['user_id' => $userId, 'ticket' => $ref],
    ], $idemKey);

    if ($status === 402) {
        unlock_user_balance($userId, $ref);   // ไม่มี order ถูกสร้าง
        throw new NotEnoughBalance($body['error']['details']);
    }
    if ($status === 409 && $body['error']['code'] === 'beneficiary_not_verified') {
        unlock_user_balance($userId, $ref);
        throw new BeneficiaryNotReady();
    }
    if ($status !== 201) {
        throw new PgError($body['error']);
    }
    return $body;
}

Response 201 ​

json
{
  "id": "wdr_01K3PA6M2T7QW9YB4H0NRSXC5D",
  "object": "withdrawal_order",
  "version": 1,
  "status": "open",
  "merchant_reference": "WD-20260821-0001",
  "end_user_ref": "u_88213",
  "beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC",
  "currency": "THB",
  "amount_total_minor": 500000,
  "fee_total_minor": 9095,
  "total_locked_minor": 509095,
  "filled_minor": 0,
  "remaining_minor": 500000,
  "fee_recognized_minor": 0,
  "settled_splits": 0,
  "max_splits": 4,
  "created_at": "2026-08-21T03:02:11.402Z",
  "expires_at": "2026-08-21T09:02:11.402Z",
  "metadata": { "user_id": "u_88213", "ticket": "WD-20260821-0001" }
}
fieldอ่านยังไง
statusopen = เข้า pool แล้ว · pending_approval = รอ back-office อนุมัติก่อน
amount_total_minorยอดที่ผู้ใช้ปลายทางจะได้รับครบเมื่อ settled
fee_total_minorค่าธรรมเนียมที่ ล็อกไว้ตั้งแต่ตอนสร้าง และไม่เปลี่ยนอีก 🔴 ยึดค่านี้ ไม่ใช่ค่าจาก quote
total_locked_minoramount_total_minor + fee_total_minor — ยอดที่หายจาก available_minor ทันที
remaining_minorยอดที่ยังหาผู้โอนไม่ได้ · ตอนสร้าง = amount_total_minor
max_splitsจำนวนก้อนสูงสุดที่ใบนี้ถูกซอยได้ · เป็นค่านโยบาย อย่า hardcode
expires_at🔴 พ้นเวลานี้ใบหลุดจาก pool · อ่านจาก response เสมอ ห้าม hardcode countdown

parser ต้องทนกับ field ที่ไม่รู้จัก

api_version เพิ่ม field ใหม่ได้โดยไม่นับเป็น breaking change และ enum หลายตัวเป็น open enum (fee_breakdown[].component, balance_transaction.type, cancel_reason_code) ⇒ ทุก switch ต้องมี default: และ parser ห้าม throw เมื่อเจอ key ที่ไม่รู้จัก

เกิดอะไรขึ้นทันทีที่ 201 ​

  1. เงินถูกหักจาก available_minor เท่ากับ total_locked_minor ตั้งแต่วินาทีนี้ ไม่ใช่ตอนจ่ายจริง
  2. webhook withdrawal.created (มี requires_approval และ reserved_balance_transaction_id)
  3. webhook balance.debited — type: "withdrawal_reserved" 🔴 event นี้ปิดการรับไม่ได้ ต้องมี handler ตั้งแต่วันแรก
json
{
  "id": "evt_01K3PA6M4V8ZR2NTY0KDXC7WH5",
  "object": "event",
  "type": "withdrawal.created",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T03:02:11.508Z",
  "sequence": 91004,
  "account": { "merchant_id": "mch_01K2G4H9RT", "white_label_id": "wl_01K1QZ2MSV" },
  "request": { "id": "req_01K3PA6M1S", "idempotency_key": "wdr-u88213-20260821-0001" },
  "data": {
    "object": {
      "id": "wdr_01K3PA6M2T7QW9YB4H0NRSXC5D",
      "object": "withdrawal_order",
      "version": 1,
      "status": "open",
      "merchant_reference": "WD-20260821-0001",
      "end_user_ref": "u_88213",
      "beneficiary_id": "ben_01K2WZ5T8Q3M7XR0VYD4NHF6BC",
      "currency": "THB",
      "amount_total_minor": 500000,
      "fee_total_minor": 9095,
      "total_locked_minor": 509095,
      "filled_minor": 0,
      "remaining_minor": 500000,
      "fee_recognized_minor": 0,
      "settled_splits": 0,
      "max_splits": 4,
      "requires_approval": false,
      "reserved_balance_transaction_id": "btxn_01K3PA6M3U9YT1QW5HN0ZDXR8C",
      "created_at": "2026-08-21T03:02:11.402Z",
      "expires_at": "2026-08-21T09:02:11.402Z",
      "metadata": { "user_id": "u_88213", "ticket": "WD-20260821-0001" }
    },
    "previous_attributes": null
  }
}

201 ไม่ได้แปลว่าเงินถึงผู้ใช้ปลายทาง

201 แปลว่า "ล็อกเงินคุณแล้ว และเอาใบนี้เข้า pool แล้ว" เท่านั้น · เงินจริงจะเข้าบัญชีปลายทางเป็นก้อน ๆ เมื่อมีผู้ฝากโอนให้

ปิดรายการในระบบคุณได้เมื่อ status = 'settled' และ remaining_minor = 0 เท่านั้น — อ่าน รับ fill หลายก้อน ก่อนเขียน handler

pending_approval — เมื่อใบนี้ต้องอนุมัติก่อน ​

ถ้านโยบายของผู้ให้บริการกำหนดให้ยอดระดับนี้ต้องอนุมัติก่อน คุณจะได้ status: "pending_approval" แทน open

ข้อเท็จจริงผล
เงินถูกล็อกไปแล้วตั้งแต่ 201ยอด available_minor ลดแล้วแม้ยังไม่อนุมัติ
ใบนี้ ยังไม่เข้า poolยังไม่มีใครโอนให้
แก้ยอด / แก้บัญชีปลายทาง ไม่ได้ต้องยกเลิกแล้วสร้างใหม่
ยกเลิกได้ คืนเต็มจำนวนดู ยกเลิกรายการถอน
ไม่อนุมัติภายใน 24 ชม.ระบบยกเลิกให้เอง (cancel_reason_code: "approval_ttl_expired") คืนเต็มจำนวน

การอนุมัติทำผ่าน POST /v1/bo/withdrawals/{id}/approve ซึ่งเป็น endpoint ฝั่ง back-office ไม่ใช่ส่วนหนึ่งของการเชื่อมต่อฝั่ง merchant

ตอนอนุมัติไม่มี webhook แจ้ง

pending_approval → open ไม่มี event เฉพาะของตัวเอง — คุณอ่าน status ได้จาก data.object.status ของ event ถัดไปเท่านั้น (ซึ่งใบแรกมักเป็น withdrawal.matched)

⇒ ถ้าหน้าจอต้องแสดงว่า "อนุมัติแล้ว" ให้ poll GET /v1/pay/withdrawals/{id} แบบ backoff 3→5→10→30 วินาที อย่า poll ทุกวินาที (pay_read = 1,200 ครั้ง/นาที ต่อ api_key)

Error ทั้งหมดของ endpoint นี้ ​

HTTPcodeเกิดเมื่อretry ได้ต้องทำอะไร
400idempotency_key_requiredไม่ส่ง Idempotency-Key❌เพิ่ม header (เขียนลง DB ก่อนยิง)
400malformed_json / unsupported_media_typebody พัง / content-type ผิด❌แก้โค้ด
401signature_invalidcanonical string ผิด❌เทียบกับ test vector
401timestamp_out_of_windowPG-Timestamp เกิน ±300 วิ✅ หลัง sync นาฬิกาตั้ง NTP
401nonce_replayedPG-Nonce ซ้ำใน 10 นาที❌gen nonce ใหม่ทุก attempt (คง Idempotency-Key เดิม)
401api_key_invalid / api_key_revokedkey ผิด/ถูกเพิกถอน❌ติดต่อผู้ให้บริการของคุณ
402insufficient_balanceavailable_minor < amount + fee · 🔴 ไม่มี order ถูกสร้าง✅ หลัง pending_clearing_available_atปลดล็อกยอดผู้ใช้คืนทันที + บอกเวลาที่ถอนได้
403tenant_suspended / scope_deniedบัญชีถูกระงับ / key ไม่มีสิทธิ์❌ติดต่อผู้ให้บริการของคุณ
409idempotency_key_reusedkey เดิมแต่ body ต่าง❌เปลี่ยน body = ต้องเปลี่ยน key
409idempotency_in_flightrequest แรกที่ใช้ key นี้ยังไม่จบ✅ (retry_after_seconds: 1)รอแล้ว retry ด้วย key เดิม
409beneficiary_not_verifiedบัญชีปลายทางยังไม่ผ่าน verify · 🔴 ไม่มี order ไม่มีการล็อกเงิน✅ หลัง verify ผ่านรอ webhook beneficiary.verified
422validation_errorfield ผิดรูป❌ดู details.fields[]
422amount_not_supportedยอดต่ำ/สูงกว่านโยบาย หรือมีเศษที่ไม่รองรับ❌ใช้ details.min_minor / details.max_minor
429rate_limitedชน bucket pay_write (600/60 วิ ต่อ api_key)✅เคารพ Retry-After + jitter ±20% · 🔴 ห้าม retry เป็นลูป
503route_unavailable / service_unavailableระบบกำลัง shed โหลด✅แสดง "ระบบหนาแน่น ลองใหม่" ตาม Retry-After
500internal_errorฝั่งเราพัง✅ส่ง request_id ให้ support

402 insufficient_balance — เคสที่พังเงียบที่สุด ​

json
{
  "error": {
    "code": "insufficient_balance",
    "type": "invalid_request_error",
    "message": "Available balance is lower than amount plus fee.",
    "message_th": "ยอดเงินที่ถอนได้ไม่พอสำหรับยอดถอนบวกค่าธรรมเนียม",
    "retryable": false,
    "retry_after_seconds": null,
    "request_id": "req_01JQA5T8V0X2Z4B6D8F0H2J4L6",
    "details": {
      "available_minor": 502000,
      "required_minor": 509095,
      "pending_clearing_minor": 480000,
      "pending_clearing_available_at": "2026-08-24T14:26:44.000Z"
    }
  }
}

dashboard มีเงิน แต่สร้างรายการถอนไม่ได้

ยอดที่เพิ่งเข้าจากรายการฝากอยู่ใน bucket pending_clearing นาน 72 ชั่วโมง ก่อนย้ายไป available · ถอนได้จาก available_minor เท่านั้น

โค้ดที่คำนวณ "เงินที่ถอนได้" จาก credited_amount_minor สะสม จะสร้างรายการถอนไม่ผ่านทั้งที่หน้าจอโชว์ว่ามีเงิน

สิ่งที่ต้องทำเพิ่ม: แยกสองยอดในระบบคุณ — แสดงผลใช้ available + pending_clearing แต่คำนวณสิทธิ์ถอนใช้ available เท่านั้น · ดึงจาก GET /v1/pay/balance · เมื่อได้ 402 ให้ ปลดล็อกยอดผู้ใช้คืนทันที แล้วบอกเวลาจาก pending_clearing_available_at

หลังจากนี้จะมี webhook อะไรตามมา ​

withdrawal.created        1 ใบ    ล็อกเงินแล้ว
balance.debited           1 ใบ    type = withdrawal_reserved   🔴 ปิดไม่ได้
withdrawal.matched        n ใบ    ทุกครั้งที่มีผู้ฝากถูกจองให้ใบนี้ (ยังไม่ใช่เงินเข้า)
withdrawal.filled         n ใบ    ต่อ 1 ก้อนที่โอนถึงจริงและตรวจสลิปผ่าน
withdrawal.settled        1 ใบ    filled_minor = amount_total_minor

withdrawal.matched ไม่ใช่เงินเข้า

matched แปลว่า "จองผู้ฝากที่จะโอนให้แล้ว" เท่านั้น ผู้ฝากอาจไม่โอน อาจโอนช้า อาจโอนแล้วสลิปไม่ผ่าน

และเพราะการตรวจสลิปฝั่งผู้ฝากเป็นแบบ รับไว้ก่อนแล้วตรวจทีหลัง (durable intake สำเร็จตอบ 202 และไม่มี 409 duplicate_slip แบบทันที) ⇒ ผลตรวจจึงมาถึงคุณช้ากว่าจังหวะที่ผู้ฝากกดส่ง และในกรณีที่ตรวจย้อนหลังพบว่าสลิปใช้ไม่ได้ ก้อนที่เคย filled แล้วยัง ถูกกลับรายการได้ ผ่าน withdrawal.fill_reversed (event นี้ก็ปิดไม่ได้)

ขั้นต่อไป ​

ถ้าคุณกำลังจะไปที่
แสดงยอดสุทธิให้ผู้ใช้เห็นก่อนกดยืนยันคำนวณค่าธรรมเนียมล่วงหน้า
เตรียมบัญชีปลายทางให้ผ่าน verifyบัญชีปลายทาง
เขียน webhook handler ของรายการถอนรับ fill หลายก้อน
ยกเลิกส่วนที่ยังไม่ถูกเติมยกเลิกรายการถอน
กัน event เก่ามาทับของใหม่ลำดับไม่การันตี — version guard
ตรวจลายเซ็นก่อนแตะ payloadตรวจลายเซ็น Ed25519

เอกสารชุดนี้อิงสัญญา API เวอร์ชัน 2026-08-01