เรื่องร้องเรียน (claims)
หน้านี้ตอบคำถามเดียว: เงินของลูกค้าออกจากบัญชีไปแล้ว แต่ในระบบไม่มีรายการที่รองรับ — จะทำอย่างไร
นี่คือจุดที่เราต่างจากตลาดมากที่สุด
เกตเวย์ที่เงินเข้าบัญชีกลางของเขา จะเห็นเงินก้อนนั้นในบัญชีตัวเองเสมอ ต่อให้ลูกค้าไม่ทำอะไรเลย ของเราเงินเข้าบัญชีของ ผู้ถอนอีกราย โดยตรง ⇒ ถ้าไม่มีสลิปมาผูก ระบบไม่มีทางรู้ว่าเงินก้อนนั้นเป็นของใคร
⇒ เราจึงมีเส้นทางที่เจ้าอื่นไม่มี: หน้าต่างรับสลิปย้อนหลัง 24 ชั่วโมง แล้วต่อด้วย ระบบเรื่องร้องเรียน ที่มีคนถือเรื่องจริง สิ่งที่คุณต้องทำเพิ่ม: ทีม support ของคุณต้องมี playbook สำหรับเคสนี้ตั้งแต่วันแรก และหน้าเว็บของคุณต้องมีปุ่ม "ฉันโอนไปแล้วแต่หมดเวลา"
เส้นเวลาของเคส "โอนแล้วแต่ไม่ได้อัปสลิป"
T+0 สร้างรายการ status = awaiting_payment แสดง QR
T+20m expires_at status = expired is_terminal = false
late_slip_accepted_until = T+20m+24h
⬑ ช่วงนี้ยังอัปสลิปได้ตามปกติ POST /slip ตอบ 202 เหมือนเดิม
T+24h20m ปิดหน้าต่าง deposit.expired ยิงซ้ำ is_terminal = true (version ใหม่)
⬑ หลังจากนี้ POST /slip ตอบ 410 พร้อม claim_url
⬑ เส้นทางเดียวที่เหลือคือเปิดเรื่องร้องเรียนexpired มา 2 ครั้ง และคุณต้องรับทั้งสองครั้ง
ครั้งแรก is_terminal: false + late_slip_accepted_until ⇒ ห้ามปิดออร์เดอร์ ครั้งที่สอง is_terminal: true ⇒ ปิดได้แล้ว ทั้งสองครั้งเป็น event ชื่อเดียวกัน ต่างกันที่ version และ is_terminal ⇒ ต้องมี version guard ไม่งั้น event เก่ามาทับใหม่ได้
สลิปที่มาช้าเกินหน้าต่าง
curl -sS -X POST "$SLIP_UPLOAD_URL" \
-H "Authorization: Bearer $SLIP_UPLOAD_TOKEN" \
-H "Idempotency-Key: slip-dep01K3M7Y9-late" \
-F "file=@slip.jpg"ใช้ upload_url จาก capability ของรายการโดยตรง ห้ามประกอบ host/path เอง และตรวจชนิด/ขนาดด้วย content_types/max_bytes ก่อนส่ง
{ "error": {
"code": "late_slip_window_closed",
"type": "invalid_request_error",
"message": "The late slip window for this deposit has closed.",
"message_th": "หมดเวลารับสลิปย้อนหลังสำหรับรายการนี้แล้ว",
"retryable": false,
"retry_after_seconds": null,
"request_id": "req_01JQA6XY7M9P1R3T5V7X9Z1B3D",
"details": {
"claim_url": "https://api.example-wl.com/v1/bo/claims",
"deposit_id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"late_slip_accepted_until": "2026-08-22T04:30:47.902Z"
} } }🔴 410 ไม่ใช่ error ที่จะ retry ให้หาย — ให้เดินต่อที่ details.claim_url ทันที อย่าปล่อยให้เคสจบตรงนี้ เงินของลูกค้าออกจากบัญชีไปแล้วจริง ๆ
เปิดเรื่องร้องเรียน
curl -sS -X POST "https://api.example-wl.com/v1/bo/claims" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "Idempotency-Key: claim-u88213-20260821-01" \
-F "deposit_id=dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B" \
-F "end_user_ref=u_88213" \
-F "amount_minor=500000" \
-F "occurred_at=2026-08-21T04:12:33.881Z" \
-F "sending_bank_code=002" \
-F "trans_ref=015520260821041233881" \
-F "description=ลูกค้าโอน 5,000 บาท ตามรายการนี้ แต่ปิดเบราว์เซอร์ก่อนอัปสลิป แนบสลิปจากแอปธนาคารมาให้แล้ว" \
-F "slip_file=@slip.jpg"เส้นทาง /v1/bo/claims* ใช้ OIDC access token ของเจ้าหน้าที่ ไม่ใช้ sk_live_* หรือ PG HMAC · POST /v1/bo/claims ใช้ claims:write และ GET /v1/bo/claims* ใช้ claims:read; การเปิด/อ่านสองแบบนี้ยังไม่ขยับเงินจึงไม่ต้อง step-up
ข้อยกเว้นคือ POST /v1/bo/claims/{id}/decisions: action refund ใช้ทั้ง claims:write และ capability payout.approve พร้อม request-bound X-PG-Step-Up-Token และ Idempotency-Key · support ปกติรับเรื่อง/ตรวจหลักฐานได้ แต่ผู้มีสิทธิ์จ่ายเงินจึงเป็นคนอนุมัติ · actor ต้อง derive จาก OIDC sub และเขียน audit log; ห้ามรับ actor_id หรือ employee id จาก request body
{
"claim_id": "clm_01K3TB4N8R2VXZ5YT0KDMWQH7P",
"version": 1,
"kind": "unmatched_transfer",
"cause": "late_transfer",
"status": "open",
"action_required_by": "support",
"deposit_id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"end_user_ref": "u_88213",
"amount_minor": 500000,
"expected_response_at": "2026-08-22T04:12:00.000Z",
"created_at": "2026-08-21T04:12:33.881Z",
"updated_at": "2026-08-21T04:12:33.881Z"
}| ฟิลด์ | บังคับ | หมายเหตุ |
|---|---|---|
deposit_id | ไม่ | ใส่ถ้ารู้ว่าเงินตั้งใจโอนให้รายการไหน · ไม่รู้ก็เว้นได้ |
end_user_ref | ใช่ | ผู้ใช้ในระบบคุณที่เป็นเจ้าของเงิน |
amount_minor | ใช่ | integer หน่วยสตางค์ ตามยอดบนสลิป |
occurred_at | ใช่ | เวลาบนสลิป · RFC3339 UTC ลงท้าย Z |
sending_bank_code | ใช่ | 3 หลัก จาก GET /v1/pay/banks |
trans_ref | ไม่ | เลขอ้างอิงจากแอปธนาคาร · มีแล้วเรื่องจบเร็วขึ้นมาก |
slip_file | ไม่ | ภาพสลิป |
description | ใช่ | เล่าให้ครบว่าเกิดอะไร ทีมที่รับเรื่องอ่านช่องนี้ก่อน |
กรอก trans_ref ให้ได้เสมอถ้าเป็นไปได้
คู่ของ (sending_bank_code, trans_ref) คือสิ่งที่ระบุธุรกรรมได้แน่นอนที่สุด และเป็นตัวเดียวกับที่ระบบใช้กันสลิปซ้ำ เคสที่มี trans_ref ตรวจได้แทบจะทันที เคสที่มีแต่ภาพเบลอ ๆ ต้องให้คนอ่าน
สถานะและผลลัพธ์ที่เป็นไปได้
status | แปลว่า | ผลกับเงิน |
|---|---|---|
open | รับเรื่องแล้ว · overpayment จะ action_required_by:end_user จนผู้ใช้ติดต่อ support | เงินถูก capture เข้าบัญชี claim แล้ว แต่ยังไม่คืน/เครดิต |
under_review | support รับเรื่องแล้ว | ยังไม่ขยับจากบัญชี claim |
refund_pending | สร้าง payout intent แล้วและกำลังยืนยันการจ่าย | liability ยังอยู่ครบ; รอธนาคารยืนยันก่อนสร้าง resolution journal |
return_pending_payout | ธนาคารคืน original transfer แล้ว แต่ payout ที่ submit ไปยังไม่ทราบผล | ห้ามกดคืนซ้ำ; ระบบ poll/cancel operation key เดิมจนได้ผล authoritative |
credited | unmatched_transfer เจอเงินและผูกกับผู้ใช้ได้ | สร้างรายการฝากใหม่แล้ว settle ⇒ คุณจะได้ deposit.settled ตามปกติ · ใช้กับ overpayment ไม่ได้ |
refunded | คืนผ่าน claim path แล้ว | liability ส่วนเกินถูกปิดด้วย resolution journal |
returned_by_bank | original transfer ถูกคืนผู้โอนแล้ว (outcome:'transfer_returned') | terminal แต่ ไม่ใช่ payout จากระบบ; ห้ามคืนซ้ำ |
rejected | unmatched_transfer ตรวจแล้วไม่พบ / ไม่ใช่ธุรกรรมที่เกี่ยวกับระบบ | ไม่มีการขยับเงิน มาพร้อมเหตุผล · ใช้กับ overpayment ไม่ได้เพราะเงินถูก verify แล้ว |
kind=overpayment รับ decision ได้เฉพาะ action:'refund' เท่านั้น · credit|reject เป็น action ของ kind=unmatched_transfer; ถ้าส่งมาให้ overpayment จะได้ 422 claim_action_not_allowed
ติดตามผลได้ 2 ทาง
GET /v1/bo/claims?kind=overpayment&status=open ผู้ใช้ติดต่อเข้ามาใหม่
GET /v1/bo/claims?kind=overpayment&status=under_review คิว support รวม payout ที่ล้มเหลว
GET /v1/bo/claims?kind=overpayment&status=refund_pending คิวติดตาม payout; ยังไม่ใช่คืนสำเร็จ
GET /v1/bo/claims?kind=overpayment&status=return_pending_payout เฝ้า race ระหว่าง transfer return กับ payout
GET /v1/bo/claims/{claim_id} ดึงสถานะล่าสุดendpoint รับ status ได้ครั้งละค่า จึงต้องดึงแต่ละคิวแยกและ paginate ให้ครบ
หรือรับ webhook claim.updated (แนะนำ) และ apply เฉพาะ claim.version ที่สูงกว่าค่าที่เก็บ
claim.updated ของ unmatched_transfer ที่ผลเป็น credited จะตามมาด้วย deposit.settled ของรายการ ใหม่
ไม่ใช่รายการเดิม — deposit_id คนละตัว handler เครดิตผู้ใช้ของคุณต้องผูกกับ deposit_id + credited_user_at IS NULL อยู่แล้ว ⇒ จะเครดิตถูกอัตโนมัติ แต่หน้าจอ "ประวัติการเติมเงิน" ของผู้ใช้จะเห็นรายการเก่าที่ล้มเหลว และ รายการใหม่ที่สำเร็จ ⇒ ต้องผูกทั้งสองใบเข้าด้วยกันด้วย claim_id ที่คุณเก็บไว้เอง ไม่งั้นลูกค้าจะโทรมาถามว่าทำไมมี 2 รายการ
เคสอื่นที่ต้องเปิดเรื่องร้องเรียน
QR ถูกแคปหน้าจอไปโอนซ้ำนอกรายการ
ผู้ใช้บันทึกภาพ QR ไว้ แล้ววันหลังโอนอีกครั้งโดยไม่ได้สร้างรายการใหม่ ⇒ เงินเข้าบัญชีผู้ถอนโดยไม่มีรายการรองรับ
- ไม่มีทางกู้อัตโนมัติ เพราะไม่มีอะไรผูกเงินก้อนนั้นกับผู้ใช้ของคุณ
- เปิดเรื่องโดย ไม่ต้องใส่
deposit_id - 🔴 ป้องกันที่หน้าเว็บ: ต้องมีข้อความ "QR นี้ใช้ได้ครั้งเดียวสำหรับรายการนี้ ห้ามบันทึกไว้" และควรใช้
checkout_urlเป็นค่าเริ่มต้นแทนการฝัง QR เอง
โอนซ้ำสำหรับรายการเดียว (เน็ตหลุด ไม่เห็นหน้ายืนยัน)
เกิด ทุกวัน และระบบเปิด claim/flag ให้อัตโนมัติ แต่ไม่คืนเงินอัตโนมัติ
- สลิปใบที่สองมี
trans_refต่างกัน ⇒ ผ่านการกันสลิปซ้ำ แต่รายการเดิม settle ไปแล้ว - ระบบ ไม่แตะรายการเดิม และเปิดเรื่องให้เอง
- คุณจะได้
deposit.overpaidโดยที่data.object.statusยังเป็นsettledเหมือนเดิม
{
"id": "evt_01K3PC9R4W7YZ2XT5NBVKD8H0M",
"object": "event",
"type": "deposit.overpaid",
"api_version": "2026-08-01",
"livemode": true,
"created_at": "2026-08-21T04:57:12.409Z",
"sequence": 88402,
"account": { "merchant_id": "mch_01K2G4H9RT5V", "white_label_id": "wl_01K1QZ2MPD7S" },
"data": {
"object": {
"id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"object": "deposit_order",
"version": 11,
"status": "settled",
"is_terminal": false,
"merchant_reference": "ORD-9981",
"amount_minor": 500000,
"created_at": "2026-08-21T04:10:47.902Z",
"ordered_amount_minor": 500000,
"transferred_amount_minor": 500000,
"amount_received_minor": 500000,
"fee_total_minor": 7500,
"fee_breakdown": [
{ "component": "processing_fee", "amount_minor": 5000, "rate_bps": 100 },
{ "component": "fixed_fee", "amount_minor": 2500 }
],
"credited_amount_minor": 492500,
"partial": false,
"settled_via": "auto",
"liquidity_source": "internal",
"balance_transaction_id": "btxn_01K3M9ZZ4P7QW2NHT0RXKD5V8C",
"settled_at": "2026-08-21T04:12:33.881Z",
"clearing_available_at": "2026-08-24T04:12:33.881Z",
"currency": "THB",
"overpayment": {
"claim_id": "clm_01K3PC9R3V6XY1WS4MAUJC7G9L",
"cause": "additional_transfer",
"amount_minor": 500000,
"slip_received_at": "2026-08-21T04:56:58.220Z",
"status": "open",
"action_required": "contact_support",
"expected_response_at": "2026-08-22T04:56:58.000Z"
}
},
"previous_attributes": { "version": 10, "overpayment": null }
}
}ห้ามเครดิตผู้ใช้เพิ่มจาก deposit.overpaid
เงินส่วนเกินไม่ใช่ยอดเครดิตของ order และไม่ใช่ของกระเป๋าคุณ status ยังเป็น settled เหมือนเดิม แปลว่ารายการเดิมถูกต้องแล้ว ห้ามย้อน transferred_amount_minor=500000 ใน snapshot ยังเป็น V ของ settlement evidence เดิม ไม่ใช่ยอดสะสม; transfer เพิ่ม 500000 อยู่ที่ overpayment.amount_minor แยกต่างหาก · ห้ามใช้สมการ V=A+E กับ cause:'additional_transfer' สิ่งที่ต้องทำ: แสดงข้อความ "รายการเดิมสำเร็จแล้ว แต่พบเงินโอนเพิ่ม กรุณาติดต่อฝ่ายบริการพร้อมเลขเรื่อง" และ overpayment.claim_id · ห้ามบอกว่าคืนเงินแล้ว
โอนเกินในสลิปใบเดียว
ถ้ายอดสั่ง X=500000, ธนาคารยืนยัน V=530000 และอีก 5 มิติผ่าน ระบบจะปิดรายการสำเร็จทันทีโดย:
- ส่ง
deposit.settledเพียงใบเดียว:transferred_amount_minor=530000,amount_received_minor=500000,partial:false - เครดิตผู้ใช้และคิด fee เฉพาะ
500000 - แนบ
overpayment{cause:'same_transfer_excess',amount_minor:30000,action_required:'contact_support'} - สร้าง claim และ capture
30000ใน transaction เดียว; ไม่ auto-refundและไม่ลง SUSPENSE
ถ้ามิติอื่น เช่นผู้โอน/ผู้รับ/เวลายังไม่ผ่าน จึงค่อยเข้า under_review; support อนุมัติภายหลังก็ยัง settle X + flag E แบบเดียวกัน
ข้อความที่ควรแสดง: รายการสำเร็จตามยอด 5,000.00 บาท แต่พบยอดโอนเกิน 300.00 บาท กรุณาติดต่อฝ่ายบริการพร้อมเลขเรื่อง CLM…
อย่าใช้ POST /v1/pay/refunds คืน overpayment
endpoint refund ปกติจะหักกระเป๋า merchant ใหม่. ยอดเกินต้องตัดสินผ่าน claim เพื่อใช้ liability/source ที่ capture ไว้และไม่หัก merchant ซ้ำ
ตัดสินคืน overpayment หลังผู้ใช้ติดต่อ support
อ่าน claim ล่าสุดก่อน แล้วส่ง expected_version ที่เจ้าหน้าที่เห็น พร้อมบัญชีผู้รับเงินของผู้โอนที่ verified แล้ว:
curl -sS -X POST "https://api.example-wl.com/v1/bo/claims/clm_01K3PB6QW1M8HN0DZ2XKQ7RV4T/decisions" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Idempotency-Key: claim-clm01K3PB6-refund-v3" \
-H "Content-Type: application/json" \
-d '{
"action": "refund",
"reason_code": "CUSTOMER_REQUESTED_REFUND",
"expected_version": 3,
"beneficiary_id": "ben_01K2R7Q4M8N0PZ3XT5VKD9WY2C",
"note": "ผู้ใช้ติดต่อ support และยืนยันบัญชีผู้โอนสำหรับรับยอดเกินคืนแล้ว"
}'reason_code:'CUSTOMER_REQUESTED_REFUND' เป็นค่าบังคับสำหรับการคืนยอดเกิน · expected_version ไม่ตรงได้ 409 stale_claim_version เพื่อกันเจ้าหน้าที่ตัดสินชนกัน · beneficiary_id ต้องเป็น sender beneficiary ที่ผ่านการยืนยันแล้ว; ยังไม่ verified ได้ 409 beneficiary_not_verified และไม่มีเงินขยับ · overpayment เป็น verified money จึงส่ง credit หรือ reject ไม่ได้ (422 claim_action_not_allowed)
คำตอบ 202 หมายถึงสร้าง payout intent สำเร็จและ Claim เปลี่ยนเป็น refund_pending เท่านั้น — ยังห้ามบอกผู้ใช้ว่าได้เงินคืนแล้ว. ระบบส่งคำสั่งจ่ายด้วย operation key เดิมทุก retry; รอ claim.updated รอบถัดไปที่ status:'refunded' และ outcome:'refund_issued' หลังธนาคารยืนยันจริง. timeout/สถานะไม่ทราบต้องคง refund_pending และ poll คำสั่งเดิม ห้ามสร้าง payout ใหม่
ถ้าธนาคารยืนยันว่า payout ล้มเหลวแบบ terminal ระบบจะไม่ลง resolution journal และส่ง claim.updated version ใหม่ที่กลับเป็น status:'under_review', action_required_by:'support'. เจ้าหน้าที่ต้องตรวจ source/beneficiary ใหม่ อ่าน version ล่าสุด แล้วค่อยส่ง decision รอบใหม่ด้วย expected_version ล่าสุดและ Idempotency-Key ใหม่ — ห้าม retry payout ที่ล้มเหลวด้วยการเดาสถานะเอง
ถ้าธนาคาร/provider คืน original transfer ถึงบัญชีผู้โอนก่อน payout จบ ระบบอาจส่ง return_pending_payout ระหว่างรอผล operation เดิม แล้วจบ returned_by_bank พร้อม outcome:'transfer_returned'. สองสถานะนี้ห้ามสร้าง decision/payout ใหม่ และ returned_by_bank ไม่ได้แปลว่าระบบโอน refund ให้—แปลว่าเงินโอนเดิมถูกตีกลับแล้ว
ขอคืนเงินให้ผู้ใช้
curl -sS -X POST "https://api.example-wl.com/v1/pay/refunds" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755840300" \
-H "PG-Nonce: 01JQA6Y53T5V7X9Z1B3D5F7H9K" \
-H "PG-Version: 2026-08-01" \
-H "Idempotency-Key: refund-dep01K3M7Y9-full" \
-H "PG-Signature: v1=5a3c1e0f8d6b4a2c0e8f6d4b2a0c8e6f4d2b0a8c6e4f2d0b8a6c4e2f0d8b6a4c" \
-H "Content-Type: application/json" \
-d '{
"deposit_id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"amount_minor": 500000,
"beneficiary_id": "ben_01K2R7Q4M8N0PZ3XT5VKD9WY2C",
"reason": "ลูกค้ายกเลิกคำสั่งซื้อและขอคืนเงินเต็มจำนวน"
}'{
"id": "wdr_01K3TC2P6S9VXZ4YT1KDMWQH8R",
"object": "withdrawal_order",
"version": 1,
"status": "open",
"kind": "refund",
"refund_of_deposit_id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"amount_total_minor": 500000,
"fee_total_minor": 3000,
"total_locked_minor": 503000,
"currency": "THB",
"expires_at": "2026-08-21T22:05:00.000Z",
"created_at": "2026-08-21T10:05:00.221Z"
}🔴 การคืนเงินคือ รายการถอนชนิดหนึ่ง ไม่ใช่การย้อนรายการฝาก
ผลที่ตามมาทั้งหมด และทุกข้อทำให้คนที่เคยต่อเจ้าอื่นมาสะดุด
- ต้องมีเงินในกระเป๋าพอ
available_minor ≥ amount + feeไม่งั้น402 insufficient_balance - เงินที่เพิ่งรับมาวันนี้ยังอยู่ในช่อง "รอพ้นหน้าต่างข้อพิพาท" ⇒ คืนเงินทันทีในวันเดียวกันมักจะไม่ผ่าน จนกว่าจะถึง
pending_clearing_available_at - มีค่าธรรมเนียมฝั่งถอน และเป็นการบวกเพิ่ม ⇒ คืน 5,000.00 ให้ลูกค้า กระเป๋าคุณลด 5,030.00
- ต้องมี
beneficiary_idที่ ผ่านการยืนยันแล้ว ไม่งั้น409 beneficiary_not_verifiedและไม่มีใบถูกสร้าง - เดิน state machine ของรายการถอนทุกประการ ⇒ รับเงินได้จากหลายผู้ฝาก ⇒
withdrawal.filledมาหลายครั้ง - event ที่คุณจะได้เป็น
withdrawal.*ทั้งหมด ไม่ใช่refund.*— แยกจากรายการถอนปกติด้วยฟิลด์refund_of_deposit_id
{ "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_01JQA6Y53T5V7X9Z1B3D5F7H9K",
"details": {
"available_minor": 12000,
"required_minor": 503000,
"pending_clearing_minor": 492500,
"pending_clearing_available_at": "2026-08-24T04:12:33.881Z"
} } }บอกลูกค้าเป็นวันที่ ไม่ใช่คำว่า "เร็ว ๆ นี้"
ใช้ pending_clearing_available_at ตรง ๆ — "คืนเงินได้ตั้งแต่ 24 ส.ค. 11:12 น." ดีกว่า "กำลังดำเนินการ" ทุกกรณี
ส่งหลักฐานเมื่อมีข้อพิพาท
ผู้ใช้ปลายทางฝั่ง รายการถอนของคุณ กด "ยังไม่ได้รับเงิน" ภายใน 72 ชั่วโมงหลังมีก้อนที่ settle
{
"id": "evt_01K3TD5Q8W1YZ4XT7NBVKD2H6M",
"object": "event",
"type": "withdrawal.disputed",
"api_version": "2026-08-01",
"livemode": true,
"created_at": "2026-08-21T12:31:44.907Z",
"sequence": 90551,
"account": { "merchant_id": "mch_01K2G4H9RT5V", "white_label_id": "wl_01K1QZ2MPD7S" },
"data": {
"object": {
"id": "wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC",
"object": "withdrawal_order",
"version": 9,
"status": "partially_filled",
"amount_total_minor": 1000000,
"filled_minor": 400000,
"remaining_minor": 600000,
"currency": "THB",
"funds_on_hold": true,
"dispute": {
"id": "dsp_01K3TD5Q7V0XY3WS6MAUJC9G4L",
"reason_code": "FUNDS_NOT_RECEIVED",
"amount_minor": 400000,
"opened_at": "2026-08-21T12:31:44.712Z",
"respond_by": "2026-08-23T12:31:44.712Z",
"evidence_url": "https://api.example-wl.com/v1/pay/disputes/dsp_01K3TD5Q7V0XY3WS6MAUJC9G4L/evidence"
}
},
"previous_attributes": { "version": 8, "dispute": null, "funds_on_hold": false }
}
}status ไม่เปลี่ยนตอนมีข้อพิพาท
withdrawal.disputed เป็น attribute-change — ใบยังเป็น partially_filled เหมือนเดิม แค่มี dispute{} และ funds_on_hold เพิ่มเข้ามา และ version ขยับ โค้ดที่ switch (event.type) แล้วเปลี่ยนสถานะตามชื่อ event จะเขียนสถานะผิด ⇒ อ่านจาก data.object.status เสมอ
ตอบข้อพิพาทก่อน respond_by
curl -sS -X POST "https://api.example-wl.com/v1/pay/disputes/dsp_01K3TD5Q7V0XY3WS6MAUJC9G4L/evidence" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755870000" \
-H "PG-Nonce: 01JQA7ZB5V7X9Z1B3D5F7H9K1M" \
-H "PG-Version: 2026-08-01" \
-H "Idempotency-Key: dispute-dsp01K3TD5Q-evidence-01" \
-H "PG-Signature: v1=0c8e6f4d2b0a8c6e4f2d0b8a6c4e2f0d8b6a4c2e0f8d6b4a2c0e8f6d4b2a0c8e" \
-H "Content-Type: application/json" \
-d '{"kind":"bank_statement_line","reference":"merchant-ledger-2026-08-21#line-009733","note":"ตรวจรายการเดินบัญชีแล้วไม่พบเงินเข้า 4,000 บาท"}'รับเฉพาะ JSON reference ไม่รับไฟล์/multipart · kind: bank_statement_line · chat_transcript · id_document_ref · other
| ผล | event ที่ตามมา | ความหมาย |
|---|---|---|
| เจ้าหน้าที่ตัดสินว่าเงินถึงจริง | withdrawal.dispute_resolved (outcome: "confirmed") | ก้อนนั้นเดินต่อตามปกติ นาฬิกา 72 ชั่วโมงเดินต่อ |
| เจ้าหน้าที่ตัดสินว่าเงินไม่ถึง | withdrawal.dispute_resolved (outcome: "reversed") แล้วตามด้วย withdrawal.fill_reversed | ก้อนนั้นถูกกลับรายการ ยอดคืนเข้าใบ |
withdrawal.fill_reversed เป็น event ที่ ปิดไม่ได้
เช่นเดียวกับ deposit.reversed และ balance.debited — เป็นการเอาเงินออกจากกระเป๋า merchant ระบบบังคับให้ endpoint ของคุณ subscribe ทั้ง 5 ตัวนี้ ปิดไม่ได้เลย: deposit.reversed · withdrawal.fill_reversed · balance.debited · webhook.key.rotating · webhook.endpoint.disabled ถ้าไม่มี handler คุณจะเห็นแต่ขาเข้า แล้วรู้ตัวตอนสิ้นเดือนว่ายอดไม่ตรง
ไม่ตอบก่อน respond_by = ยอมรับผลอัตโนมัติ
respond_by ปกติอยู่ที่ ~48 ชั่วโมงหลังเปิดเรื่อง — ตั้ง alert ที่ครึ่งทาง ไม่ใช่ก่อนหมดเวลา 1 ชั่วโมง
ถ้าคุณเป็นฝั่งผู้ฝาก คุณจะไม่ได้ event ของข้อพิพาท
เมื่อผู้ถอนอีกฝั่งเปิดข้อพิพาทกับก้อนที่จับคู่กับรายการฝากของคุณ เงินที่เพิ่งเครดิตให้คุณจะ ถูกพักไว้ จนกว่าเรื่องจะจบ
- รายการฝากของคุณยังเป็น
settledเหมือนเดิม ไม่มี event ใหม่ - แต่
balance.clearedที่ควรมาถึงตอน 72 ชั่วโมง จะไม่มา จนกว่าเรื่องจะตัดสิน - ⇒ 🔴 อย่าใช้
available_atเป็นตัวขับ logic · ตัวเลขที่ตัดสินใจได้จริงมีตัวเดียวคือavailable_minorจากGET /v1/pay/balance
ถ้าเรื่องจบด้วยการกลับรายการ คุณจะได้ deposit.reversed + balance.debited โดยตัดจากช่อง pending_clearing ⇒ กระเป๋าคุณไม่ติดลบ
เช็กลิสต์งานเรื่องร้องเรียน
- [ ] หน้าเว็บมีปุ่ม "ฉันโอนไปแล้วแต่หมดเวลา" จนถึง
late_slip_accepted_until - [ ] handle
410 late_slip_window_closedโดยเปิดเรื่องต่อ ไม่ใช่แสดง error แล้วจบ - [ ] เก็บ
claim_idผูกกับผู้ใช้ และในเคสunmatched_transferให้ผูกรายการเก่ากับรายการใหม่ที่เกิดจากcredited - [ ] subscribe
claim.updated - [ ] handle
deposit.overpaidโดยไม่เครดิตเพิ่ม - [ ] decision ของ overpayment ใช้เฉพาะ
refund+reason_code+ latestexpected_version+ verified senderbeneficiary_id; ต้องมีpayout.approve,credit/rejectต้องได้422 - [ ] แยก
202/refund_pendingออกจากrefunded/refund_issued; แจ้งว่าสำเร็จหลังclaim.updatedรอบยืนยันจากธนาคารเท่านั้น และรองรับ terminal failure ที่กลับunder_review/supportโดยไม่ลง journal - [ ] มี handler ครบทั้ง 5 event ที่ปิดไม่ได้
- [ ] มี alert เมื่อมี
withdrawal.disputedที่ยังไม่ตอบและเหลือเวลาไม่ถึงครึ่งของหน้าต่าง - [ ] ทีม support มี playbook:
under_review= รอตรวจ ไม่ใช่ error · ห้ามแนะนำให้โอนซ้ำ
ขั้นต่อไป
- กระเป๋าเงินและยอดคงเหลือ — ทำไมคืนเงินวันเดียวกันมักไม่ผ่าน
- ค้นหารายการ — แนบหลักฐานเข้าเรื่องที่เปิดอยู่ และ compatibility
force-completeสำหรับ short-pay ที่มีเหตุ review อื่นเท่านั้น - อัปโหลดสลิป — durable intake สำเร็จตอบ 202 — หน้าต่างรับสลิปย้อนหลัง
- รายการ event ทั้งหมด —
claim.updated·deposit.overpaid·withdrawal.disputed - บันทึกและ audit log — ตรวจว่า event ที่เกี่ยวกับเรื่องนี้ส่งถึงคุณครบไหม
