Skip to content

เรื่องร้องเรียน (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 เก่ามาทับใหม่ได้

สลิปที่มาช้าเกินหน้าต่าง ​

bash
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 ก่อนส่ง

json
{ "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 ทันที อย่าปล่อยให้เคสจบตรงนี้ เงินของลูกค้าออกจากบัญชีไปแล้วจริง ๆ

เปิดเรื่องร้องเรียน ​

bash
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

json
{
  "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_reviewsupport รับเรื่องแล้วยังไม่ขยับจากบัญชี claim
refund_pendingสร้าง payout intent แล้วและกำลังยืนยันการจ่ายliability ยังอยู่ครบ; รอธนาคารยืนยันก่อนสร้าง resolution journal
return_pending_payoutธนาคารคืน original transfer แล้ว แต่ payout ที่ submit ไปยังไม่ทราบผลห้ามกดคืนซ้ำ; ระบบ poll/cancel operation key เดิมจนได้ผล authoritative
creditedunmatched_transfer เจอเงินและผูกกับผู้ใช้ได้สร้างรายการฝากใหม่แล้ว settle ⇒ คุณจะได้ deposit.settled ตามปกติ · ใช้กับ overpayment ไม่ได้
refundedคืนผ่าน claim path แล้วliability ส่วนเกินถูกปิดด้วย resolution journal
returned_by_bankoriginal transfer ถูกคืนผู้โอนแล้ว (outcome:'transfer_returned')terminal แต่ ไม่ใช่ payout จากระบบ; ห้ามคืนซ้ำ
rejectedunmatched_transfer ตรวจแล้วไม่พบ / ไม่ใช่ธุรกรรมที่เกี่ยวกับระบบไม่มีการขยับเงิน มาพร้อมเหตุผล · ใช้กับ overpayment ไม่ได้เพราะเงินถูก verify แล้ว

kind=overpayment รับ decision ได้เฉพาะ action:'refund' เท่านั้น · credit|reject เป็น action ของ kind=unmatched_transfer; ถ้าส่งมาให้ overpayment จะได้ 422 claim_action_not_allowed

ติดตามผลได้ 2 ทาง

http
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 เหมือนเดิม
json
{
  "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 แล้ว:

bash
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 ให้—แปลว่าเงินโอนเดิมถูกตีกลับแล้ว

ขอคืนเงินให้ผู้ใช้ ​

bash
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": "ลูกค้ายกเลิกคำสั่งซื้อและขอคืนเงินเต็มจำนวน"
  }'
json
{
  "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"
}

🔴 การคืนเงินคือ รายการถอนชนิดหนึ่ง ไม่ใช่การย้อนรายการฝาก

ผลที่ตามมาทั้งหมด และทุกข้อทำให้คนที่เคยต่อเจ้าอื่นมาสะดุด

  1. ต้องมีเงินในกระเป๋าพอ available_minor ≥ amount + fee ไม่งั้น 402 insufficient_balance
  2. เงินที่เพิ่งรับมาวันนี้ยังอยู่ในช่อง "รอพ้นหน้าต่างข้อพิพาท" ⇒ คืนเงินทันทีในวันเดียวกันมักจะไม่ผ่าน จนกว่าจะถึง pending_clearing_available_at
  3. มีค่าธรรมเนียมฝั่งถอน และเป็นการบวกเพิ่ม ⇒ คืน 5,000.00 ให้ลูกค้า กระเป๋าคุณลด 5,030.00
  4. ต้องมี beneficiary_id ที่ ผ่านการยืนยันแล้ว ไม่งั้น 409 beneficiary_not_verified และไม่มีใบถูกสร้าง
  5. เดิน state machine ของรายการถอนทุกประการ ⇒ รับเงินได้จากหลายผู้ฝาก ⇒ withdrawal.filled มาหลายครั้ง
  6. event ที่คุณจะได้เป็น withdrawal.* ทั้งหมด ไม่ใช่ refund.* — แยกจากรายการถอนปกติด้วยฟิลด์ refund_of_deposit_id
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_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

json
{
  "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 ​

bash
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 + latest expected_version + verified sender beneficiary_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 · ห้ามแนะนำให้โอนซ้ำ

ขั้นต่อไป ​

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