Skip to content

อนุมัติรายการถอน ​

รายการถอนบางใบเข้าสถานะ pending_approval ก่อน ต้องมีคนกดอนุมัติในระบบหลังบ้านถึงจะเข้าสู่กระบวนการจับคู่

เงินถูกล็อกตั้งแต่ตอน สร้าง ไม่ใช่ตอนอนุมัติ

POST /v1/pay/withdrawals หักยอด amount + fee ออกจาก available_minor ทันที ทั้งเส้นที่ต้องอนุมัติและไม่ต้อง คุณจะได้ balance.debited (type: "withdrawal_reserved") ตั้งแต่วินาทีที่สร้าง — ก่อนที่ใครจะกดอนุมัติ

เหตุผล: ถ้าล็อกตอนอนุมัติ ผู้ใช้จะสร้างใบถอนพร้อมกัน 10 ใบด้วยเงินก้อนเดียวได้ แล้วพอ approve ทีละใบจะติดลบ

รายการไหนต้องอนุมัติ ​

ดูจาก response ของการสร้าง

bash
curl -sS -X POST "https://api.example-wl.com/v1/pay/withdrawals" \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "PG-Timestamp: 1755749553" \
  -H "PG-Nonce: 01JQA5T8V0X2Z4B6D8F0H2J4L6" \
  -H "PG-Version: 2026-08-01" \
  -H "Idempotency-Key: wdr-u88213-20260821-01" \
  -H "PG-Signature: v1=8f2c1ab6e0d74f9b3c5a7e1d0f6b8a4c2e9d7b5a3c1f0e8d6b4a2c0e8f6d4b2a" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 1000000,
    "currency": "THB",
    "merchant_reference": "WD-20260821-0042",
    "end_user_ref": "u_88213",
    "beneficiary_id": "ben_01K2R7Q4M8N0PZ3XT5VKD9WY2C",
    "client": {
      "device_fingerprint": "df_9a1c4e77b2",
      "ip": "203.0.113.44",
      "user_agent": "Mozilla/5.0"
    },
    "metadata": { "user_id": "u_88213" }
  }'
json
{
  "id": "wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC",
  "object": "withdrawal_order",
  "version": 1,
  "status": "pending_approval",
  "amount_total_minor": 1000000,
  "fee_total_minor": 3000,
  "total_locked_minor": 1003000,
  "currency": "THB",
  "expires_at": "2026-08-21T16:12:00.000Z",
  "created_at": "2026-08-21T04:12:00.118Z"
}
status ที่ได้แปลว่าต้องทำอะไรต่อ
pending_approvalรอคนกดอนุมัติเข้าคิวในระบบหลังบ้าน
openไม่ต้องอนุมัติ เข้าสู่การจับคู่แล้วรอ withdrawal.matched / withdrawal.filled

webhook withdrawal.created มีฟิลด์ requires_approval และ reserved_balance_transaction_id (ใบที่กันยอด amount + fee ไว้)

total_locked_minor = ยอด + ค่าธรรมเนียม ไม่ใช่แค่ยอด

ค่าธรรมเนียมฝั่งถอนเป็นการ บวกเพิ่ม ไม่ใช่หักจากยอดที่ผู้รับได้ ถอน 10,000.00 ค่าธรรมเนียม 30.00 ⇒ ผู้รับได้ 10,000.00 เต็ม · กระเป๋าคุณลด 10,030.00 ถ้า available_minor ไม่ถึง 1003000 จะได้ 402 insufficient_balance และ ไม่มีใบถูกสร้าง ไม่มีการล็อกเงิน

อนุมัติ ​

bash
curl -sS -X POST "https://api.example-wl.com/v1/bo/withdrawals/wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC/approve" \
  -H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
  -H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
  -H "Idempotency-Key: approve-wdr01K3PB2M7Q4RW8YT0NBVXH5DKC" \
  -H "Content-Type: application/json" \
  -d '{
    "note": "ยอดตรงกับคำขอถอนของลูกค้าในระบบ ตรวจบัญชีปลายทางแล้ว",
    "reason_code": "WITHDRAWAL_DETAILS_VERIFIED"
  }'

/v1/bo/* ใช้ OIDC access token ของเจ้าหน้าที่ ไม่ใช้ sk_live_* หรือ PG HMAC และ action นี้ต้องมี step-up token ที่ยังไม่หมดอายุ ระบบระบุตัวผู้อนุมัติจาก claim sub ใน OIDC token — ห้ามรับ actor_id หรือ approver id จาก request body

ขอบเขต idempotency รวม tenant + OIDC principal ที่ verify แล้ว (issuer/sub ที่ map เป็น tenant user) + livemode + method/path ดังนั้น retry จาก คนเดิม ด้วย literal key/body เดิม replay ผลเดิม แต่ผู้อนุมัติคนที่สองซึ่งเป็นคนละ principal จะใช้ literal key เดียวกันก็เป็นคนละ operation ได้ · step-up เป็น one-time credential ที่ bind กับ subject, resource/action, Idempotency-Key และ body digest; หลัง operation สำเร็จแล้ว การ replay ด้วย scope/key/body เดิมไม่ต้องทำ MFA ใหม่

json
{
  "id": "wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC",
  "object": "withdrawal_order",
  "version": 2,
  "status": "open",
  "amount_total_minor": 1000000,
  "fee_total_minor": 3000,
  "filled_minor": 0,
  "remaining_minor": 1000000,
  "approved_at": "2026-08-21T04:14:52.330Z",
  "expires_at": "2026-08-21T16:12:00.000Z"
}

ไม่มี event withdrawal.approved และจะไม่มี

การอนุมัติไม่ทำให้เงินขยับ (ล็อกไปตั้งแต่ตอนสร้างแล้ว) ⇒ ไม่มี journal ⇒ ไม่มี event เฉพาะ

คุณรู้ว่าใบถูกอนุมัติแล้วจาก data.object.status ที่เปลี่ยนเป็น open ใน event ถัดไปของใบนั้น (เช่น withdrawal.matched) หรือจาก GET /v1/pay/withdrawals/{id} โค้ดที่รอ event ชื่อ withdrawal.approved จะรอตลอดไป

เงื่อนไขที่ต้องผ่านครบก่อนอนุมัติได้ ​

#เงื่อนไขทำไม
1ผู้อนุมัติต้องไม่ใช่คนสร้างใบคนเดียวสร้างแล้วอนุมัติเองได้ = ไม่มีการควบคุมเลย
2step-up MFA ภายใน 5 นาที ก่อนกดเครื่องที่ล็อกอินค้างไว้ไม่พอสำหรับการจ่ายเงินออก
3ผู้อนุมัติที่สองถ้ายอดเกินเพดาน (ดูหัวข้อถัดไป)ยอดใหญ่ต้องมีคนที่สอง
4บัญชีทุกชั้นในสายต้องยังไม่ถูกระงับถ้าชั้นใดถูกระงับจะได้ 403 tenant_suspended
5note มีความยาว อย่างน้อย 20 ตัวอักษรตรวจที่ฐานข้อมูล ไม่ผ่านได้ 422 review_note_required

"อนุมัติแล้ว" ไม่ได้แปลว่า "จ่ายเงินแล้ว"

open แปลว่าใบเข้าไปอยู่ในกระบวนการจับคู่ ยังไม่มีใครโอนให้แม้แต่บาทเดียว ปิดรายการในระบบคุณได้เมื่อ status = 'settled' และ remaining_minor = 0 เท่านั้น

เพดานที่ต้องมีผู้อนุมัติสองคน ​

ยอดที่เกินเพดานที่ชั้นบนกำหนดไว้ ต้องมีผู้อนุมัติคนที่สองที่ ไม่ใช่คนแรก จึงจะเดินต่อได้

ผู้อนุมัติแต่ละคนต้องเรียก approve แยกคนละ request ด้วย OIDC access token และ one-time step-up token ของตัวเอง ห้ามส่งผู้อนุมัติสองคนมาใน request เดียว ทั้งสองคนใช้ literal Idempotency-Key เดียวกันได้เพราะ server scope ด้วย principal; ห้ามแก้ body เพื่อบังคับให้ request แยก

ยอดต้องมีกี่คนผลของการกด approve ครั้งแรก
ไม่เกินเพดาน1 คนstatus เปลี่ยนเป็น open ทันที
เกินเพดาน2 คนใบยังคง pending_approval จนกว่าคนที่สองจะกด

สิ่งที่ระบบหลังบ้านของคุณต้องมี

  1. คิวที่แยก "รอคนแรก" กับ "รอคนที่สอง" ให้เห็น ไม่งั้นใบใหญ่จะค้างโดยไม่มีใครรู้
  2. ปุ่มอนุมัติต้องเทาสำหรับคนที่กดไปแล้ว — ไม่ใช่ปล่อยให้กดซ้ำแล้วค่อยขึ้น error
  3. นาฬิกา 24 ชั่วโมง ที่นับจากเวลาสร้าง (ดูหัวข้อถัดไป) เพราะใบที่รอคนที่สองก็หมดอายุเหมือนกัน
  4. บันทึกว่าใครกด กดเมื่อไร และ note ว่าอะไร — ดู บันทึกและ audit log

ค่าเพดานไม่ได้ถูกส่งออกมาใน webhook

เราไม่ส่งตัวเลขเพดานออกไปกับ payload ของ event ⇒ อย่าพยายาม parse หาจาก event ดูค่าปัจจุบันจากหน้า ตั้งค่า และปรับได้ ภายในเพดานที่ชั้นบนกำหนด เท่านั้น

ปฏิเสธ ​

bash
curl -sS -X POST "https://api.example-wl.com/v1/bo/withdrawals/wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC/reject" \
  -H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
  -H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
  -H "Idempotency-Key: reject-wdr01K3PB2M7Q4RW8YT0NBVXH5DKC" \
  -H "Content-Type: application/json" \
  -d '{
    "note": "ลูกค้ายกเลิกคำขอถอนทางแชทก่อนเวลา 11:20 น. ยืนยันตัวตนแล้ว",
    "reason_code": "END_USER_CANCELED"
  }'

ผลที่เกิด

  • ใบไปที่ rejected — ทางเดียวที่เข้าสถานะนี้ได้คือถูกปฏิเสธตอน pending_approval
  • คืนเงิน เต็มจำนวน amount + fee เข้า available_minor
  • webhook: withdrawal.rejected (มี rejection.code + ข้อความ) และ balance.credited (type: "withdrawal_released")
  • สร้างใบใหม่ได้ทันที ใบเดิมไม่กลับมา

note สั้นกว่า 20 ตัวอักษรจะได้

json
{ "error": {
    "code": "review_note_required",
    "type": "invalid_request_error",
    "message": "A decision note of at least 20 characters is required.",
    "message_th": "ต้องระบุเหตุผลอย่างน้อย 20 ตัวอักษร",
    "retryable": false,
    "retry_after_seconds": null,
    "request_id": "req_01JQA5TD5Y3A7E9G1J3L5N7Q9S",
    "details": { "field": "note" } } }

ใบที่ไม่มีใครกดภายใน 24 ชั่วโมง ​

ใบที่ค้าง pending_approval เกิน 24 ชั่วโมงจะถูกยกเลิกอัตโนมัติและคืนเงินเต็มจำนวน

json
{
  "id": "evt_01K3RC7T2V5XZ8YB4KDMWQH0PL",
  "object": "event",
  "type": "withdrawal.canceled",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-22T04:12:00.442Z",
  "sequence": 90117,
  "account": { "merchant_id": "mch_01K2G4H9RT5V", "white_label_id": "wl_01K1QZ2MPD7S" },
  "data": {
    "object": {
      "id": "wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC",
      "object": "withdrawal_order",
      "version": 3,
      "status": "canceled",
      "canceled_by": "system",
      "cancel_reason_code": "approval_ttl_expired",
      "amount_total_minor": 1000000,
      "fee_total_minor": 3000,
      "filled_minor": 0,
      "refunded_minor": 1000000,
      "fee_recognized_minor": 0,
      "currency": "THB"
    },
    "previous_attributes": { "version": 1, "status": "pending_approval" }
  }
}

ตั้ง alert ก่อนถึง 24 ชั่วโมง

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

ยกเลิกเองระหว่างรออนุมัติ ​

bash
curl -sS -X POST "https://api.example-wl.com/v1/pay/withdrawals/wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC/cancel" \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "PG-Timestamp: 1755749760" \
  -H "PG-Nonce: 01JQA5TF7A5C9E1G3J5L7N9Q1S" \
  -H "PG-Version: 2026-08-01" \
  -H "Idempotency-Key: cancel-wdr01K3PB2M7Q4RW8YT0NBVXH5DKC" \
  -H "PG-Signature: v1=6d2b90f4a8c1e37b5d9f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2b" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "ลูกค้าเปลี่ยนบัญชีปลายทาง" }'

คืนเงินเต็มจำนวน · withdrawal.canceled + balance.credited

แก้ยอดหรือแก้บัญชีปลายทางของใบที่ pending_approval ไม่ได้

ไม่มี endpoint สำหรับแก้ และจะไม่มี — ยกเลิกแล้วสร้างใบใหม่ เท่านั้น เหตุผล: เงินถูกล็อกด้วยยอดเดิมไปแล้ว การแก้ยอดกลางคันแปลว่าต้องปลดล็อกและล็อกใหม่ในทรานแซกชันเดียวกับการเปลี่ยนบัญชีปลายทาง ซึ่งเป็นจุดที่เงินหายได้จริง

หลังอนุมัติแล้วจะเจออะไรต่อ ​

1 ใบถอนรับเงินได้จาก หลายรายการฝาก ⇒ webhook มาหลายครั้ง ไม่ใช่ครั้งเดียวตอนจบ

withdrawal.matched   (ก้อนที่ 1 · 4,000.00)   ← ยิงทุกครั้งที่มีคนจับจอง ไม่ใช่ครั้งแรกครั้งเดียว
withdrawal.filled    (filled 400000 · remaining 600000)
withdrawal.matched   (ก้อนที่ 2 · 6,000.00)
withdrawal.filled    (filled 1000000 · remaining 0)
withdrawal.settled

withdrawal.filled ไม่ใช่จุดจบ

อ่านยอดจาก data.object.filled_minor / remaining_minor ผ่าน version guard เสมอ ห้ามใช้ SUM() ของตาราง fill ฝั่งคุณมาขับ logic — event อาจยังมาไม่ครบ และลำดับไม่การันตี ตาราง fill มีไว้ทำ timeline เท่านั้น โดยใช้ leg_ref เป็น key

รายละเอียดเต็มอยู่ที่ หนึ่งใบรับได้หลายก้อน (partial fill)

Error ที่เจอบ่อยที่หน้านี้ ​

HTTPcodeเกิดเมื่อทำอย่างไร
402insufficient_balanceavailable_minor ไม่ถึง amount + fee ตอน สร้างปลดล็อกยอดผู้ใช้คืนทันที · บอกเวลาด้วย details.pending_clearing_available_at
409withdrawal_has_pending_fillสั่งยกเลิกตอนที่มีคนกำลังโอนให้อยู่รอตาม retry_after_seconds แล้วลองใหม่ — 🔴 ห้ามยึดเงินคนที่กำลังโอน
409withdrawal_not_cancelableใบ settled/expired/canceled ไปแล้วอ่านสถานะล่าสุดจาก GET
409withdrawal_state_conflictสถานะขยับไปแล้วระหว่างที่คุณกดอ่านสถานะล่าสุดจาก GET แล้วตัดสินใจใหม่
409beneficiary_not_verifiedบัญชีผู้รับยังไม่ผ่านการยืนยันรอ webhook beneficiary.verified — ไม่มีใบถูกสร้าง ไม่มีการล็อกเงิน
422review_note_requirednote สั้นกว่า 20 ตัวอักษรเขียนเหตุผลให้ครบ
403tenant_suspendedบัญชีชั้นใดชั้นหนึ่งในสายถูกระงับติดต่อผู้ให้บริการของคุณ

เช็กลิสต์หน้าคิวอนุมัติ ​

  • [ ] แสดง total_locked_minor (ยอด + ค่าธรรมเนียม) ไม่ใช่แค่ยอดถอน
  • [ ] แยกคิว "รอคนแรก" กับ "รอคนที่สอง"
  • [ ] นับถอยหลัง 24 ชั่วโมงจาก created_at ของใบ พร้อม alert ที่ 4 ชั่วโมง
  • [ ] บังคับ note อย่างน้อย 20 ตัวอักษรที่ฝั่ง UI ด้วย ไม่ใช่รอ 422
  • [ ] ปุ่มอนุมัติเทาสำหรับคนที่สร้างใบและคนที่กดไปแล้ว
  • [ ] อ่านผลจาก data.object.status ไม่ใช่รอ event ชื่อ withdrawal.approved
  • [ ] เก็บ Idempotency-Key + body snapshot แยกตาม OIDC principal ก่อน ยิง; retry คนเดิมใช้ค่าเดิม ส่วนคนที่สองเป็น operation ใหม่แม้ literal key เหมือนกัน

ขั้นต่อไป ​

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