Skip to content

แคตตาล็อก event ทั้งหมด ​

นี่คือรายการเดียวในระบบ · ไม่มี event ที่ไม่อยู่ในตารางนี้

type เป็น enum เปิด

วันหนึ่งจะมีชื่อ event ใหม่โผล่มาโดยไม่นับเป็น breaking change · switch (event.type) ทุกตัวต้องมี default: ที่ ตอบ 200 แล้วบันทึกไว้เฉย ๆ ไม่ใช่ throw

ตาราง event ​

ฝั่งรายการฝาก (deposit.*) ​

Eventเกิดเมื่อไรปิดได้payload ที่ต่างจากใบอื่น
deposit.createdทุกครั้งที่สร้างรายการสำเร็จ — ทั้งเส้น 201 และเส้น 202✅data.previous_attributes = null (event เดียวที่เป็น null เสมอ) · request.idempotency_key · data.object.status เป็นค่าจริง (awaiting_payment หรือ awaiting_liquidity)
deposit.awaiting_liquidityยังไม่มีคู่จับคู่ · ยิงหลัง created เป็น attribute-change✅liquidity.wait_started_at · liquidity.estimated_wait_ms · liquidity.retry_interval_ms · liquidity.suggested_amounts_minor[]
deposit.matchedได้ปลายทางให้โอนแล้ว✅legs[] (QR + ปลายทาง + expires_at ต่อ leg) · liquidity_source · leg_count — event เดียวที่มี QR
deposit.leg_updatedปลายทางบาง leg เปลี่ยนสถานะ✅legs[{leg_ref, sequence, total, leg_status}]
deposit.slip_receivedรับสลิปแล้ว (ตอบ 202 ไปแล้ว)✅slip.received_at · slip.is_late · 🔴 ยังไม่มี verification — ห้ามปล่อยของ
deposit.slip_rejectedรูปอ่านไม่ออก ขอรูปใหม่✅retryable: true · rejection.code = 'SLIP_UNREADABLE' · attempts_left — 🔴 ไม่ใช่การปฏิเสธเงิน
deposit.under_reviewมิติ non-amount ไม่ผ่าน / ติด policy / ธนาคารปิดตรวจ / สลิปซ้ำ / กำกวม; amount mismatch อย่างเดียวไม่เข้าคิว✅review.reasons[] · review.opened_at · review.escalated · review.four_eyes_required · review.estimated_resolution_bucket · verification{} boolean 6 ตัว
deposit.settledเครดิตแล้ว รวม clean short-pay และ same-slip overpay✅snapshot การเงินครบ + overpayment เสมอ (null เมื่อไม่เกิน, object เมื่อเกิน) · clean short-pay ส่ง A=V<X, partial:true, settled_via:'auto'; same-slip overpay ส่งใบนี้เพียงใบเดียว
deposit.overpaidมี transfer เพิ่มหลังรายการ settle ไปแล้ว✅settled snapshot ครบ (ordered/transferred/received/fee/credited/partial) โดยยอดเดิมไม่เปลี่ยน + overpayment{claim_id, cause:'additional_transfer', amount_minor, slip_received_at, status, action_required} — 🔴 status ยัง settled; ไม่มี auto-refund
deposit.expiredหมดเวลาโดยไม่มีสลิป✅late_slip_accepted_until · is_terminal · expired_reason ∈ no_payment
deposit.canceledยกเลิกก่อนโอน — คุณกด / ผู้ใช้กด / ระบบหมดทางจัดหาสภาพคล่อง✅canceled_by ∈ merchant | end_user | system · cancel_reason_code · late_slip_accepted_until (อาจเป็น null) · is_terminal
deposit.rejectedเจ้าหน้าที่ตัดสินไม่รับสลิปใบนี้✅rejection.code + rejection.display_th / display_en
deposit.reversedเคย settle แล้วถูกกลับรายการ🔴 ไม่ได้reversal.reason_code · reversal.reverses_balance_transaction_id · reversal.balance_transaction_id

ฝั่งรายการถอน (withdrawal.*) ​

Eventเกิดเมื่อไรปิดได้payload ที่ต่างจากใบอื่น
withdrawal.createdสร้างรายการถอนสำเร็จ✅requires_approval · reserved_balance_transaction_id (ยอด amount + fee ที่กันไว้)
withdrawal.matchedทุกครั้งที่มีคนจองยอดจะโอนให้✅matched_legs · reserved_minor · 🔴 ไม่มี id ของรายการฝากฝั่งตรงข้าม
withdrawal.filledได้รับเงินจริง 1 ก้อน✅fill{leg_ref, sequence, amount_minor, fee_recognized_minor, settled_at, balance_transaction_id, auto_confirm_at} · filled_minor · remaining_minor · settled_splits / max_splits — 🔴 event เดียวที่ยิงหลายครั้งต่อ 1 รายการ
withdrawal.settledได้รับครบยอด✅fills[] สรุปครบทุกก้อน · fee_total_minor · dust_absorbed
withdrawal.expiredหมดเวลาโดยยังเหลือยอด✅filled_minor / refunded_minor / fee_recognized_minor · released_balance_transaction_id
withdrawal.canceledยกเลิกส่วนที่เหลือ✅canceled_by · refunded_minor
withdrawal.rejectedถูกปฏิเสธตอนรออนุมัติ✅rejection.code + display
withdrawal.receipt_confirmedผู้ถอนยืนยันว่าได้รับเงินก้อนนั้น✅fill.leg_ref · confirmed_by ∈ withdrawer | auto
withdrawal.disputedผู้ถอนแจ้งว่าไม่ได้รับเงิน✅dispute{id, reason_code, amount_minor, opened_at, respond_by, evidence_url} · funds_on_hold: true · 🔴 status ไม่เปลี่ยน
withdrawal.dispute_resolvedตัดสินข้อพิพาทแล้ว✅dispute{id, outcome ∈ confirmed | reversed}
withdrawal.fill_reversedเงินก้อนที่จ่ายไปแล้วถูกกลับรายการ🔴 ไม่ได้fill.leg_ref · reversal.reason_code · replacement_withdrawal_id (ถ้ามี)

ฝั่งกระเป๋าเงิน (balance.*) ​

Eventเกิดเมื่อไรปิดได้payload ที่ต่างจากใบอื่น
balance.creditedทุกครั้งที่มีเงินเข้ากระเป๋า✅data.object เป็น balance_transaction (immutable — ไม่มี version) · bucket_to · available_at
balance.clearedครบ 72 ชม. ยอดย้ายจากรอเคลียร์เป็นถอนได้✅cleared_from_balance_transaction_id · bucket_from: 'pending_clearing' · bucket_to: 'available'
balance.debitedทุกครั้งที่มีเงินออกจากกระเป๋า🔴 ไม่ได้type ∈ withdrawal_reserved | withdrawal_settled | reversal | fee | adjustment · balance_after{}

balance.cleared ไม่ใช่เงินเข้าใหม่

มันคือการย้าย bucket จาก pending_clearing → available ของยอดเดิม

merchant ที่บวกยอดตาม event จะได้ยอด สองเท่า ถ้าเผลอนับใบนี้เป็นเงินเข้า · เหตุผลที่เราไม่ยิง balance.credited ซ้ำที่ T+72h ก็คือข้อนี้ตรง ๆ

ฝั่งระบบ (webhook.*) — ปิดไม่ได้ทั้งคู่ ​

Eventเกิดเมื่อไรปิดได้payload ที่ต่างจากใบอื่น
webhook.key.rotatingประกาศ kid ใหม่ล่วงหน้า 7 วัน🔴 ไม่ได้next_kid · next_public_key · next_raw_public_key · activates_at — ⚠️ เซ็นด้วยกุญแจปัจจุบัน ไม่ใช่กุญแจใหม่
webhook.endpoint.disabledระบบปิด endpoint อัตโนมัติหลังล่มติดกัน 24 ชม.🔴 ไม่ได้ส่งไป endpoint กลุ่มอื่นที่ยังดี + อีเมลถึง technical contact

event ที่ยังไม่มีรายละเอียด payload ในสัญญา ​

สามตัวนี้อยู่ในรายการชื่อ event ที่คุณต้องรองรับ และมีที่มาชัดเจน แต่ยังไม่มีสเปก payload เต็ม:

Eventเกิดเมื่อไรที่รู้แน่
beneficiary.verifiedบัญชีผู้รับเงินถอนผ่านการตรวจชื่อกับธนาคาร (ปกติ < 30 วิ)ต้องรอใบนี้ก่อนสร้างรายการถอนด้วย beneficiary_id นั้น
beneficiary.rejectedตรวจไม่ผ่านcode ∈ NAME_MISMATCH | ACCOUNT_NOT_FOUND | BANK_UNAVAILABLE
claim.updatedเรื่องที่เปิดไว้ (รวม overpayment) มีความคืบหน้าClaim.version เพิ่มเสมอ; เส้นปกติ open/end_user → refund_pending/none → refunded/refund_issued; terminal payout failure กลับ under_review/support โดยไม่มี journal; original transfer return อาจเดิน return_pending_payout/none → returned_by_bank/transfer_returned และไม่ใช่ payout จากระบบ · ไม่มี auto-refund

⇒ เขียน handler ให้ตกลง default: ได้อย่างปลอดภัย แล้วอ่านสถานะจริงจาก endpoint ที่เกี่ยวข้อง

event ที่ไม่เปลี่ยน status (attribute-change) ​

5 ตัวนี้ยิงโดย สถานะเดิมไม่ขยับ — ถ้า handler ของคุณตัดสินใจจาก event.type อย่างเดียวจะเข้าใจผิดทันที

eventเปลี่ยนอะไรทำไมไม่มีสถานะใหม่
deposit.awaiting_liquidityliquidity{}สถานะยังเป็น awaiting_liquidity เหมือนเดิม
deposit.under_review (ซ้ำใบที่สอง)review.escalated = trueรอนานเกินเกณฑ์ต้องไม่แตะเงิน
deposit.overpaidแนบ latest overpayment{} จาก transfer เพิ่มรายการเดิม settle ถูกต้องแล้ว ห้ามย้อน · same-slip flagมากับ deposit.settled
withdrawal.matchedreserved_minorการจองยอดห้ามเลื่อนสถานะ
withdrawal.disputedแนบ dispute{}ข้อพิพาทเกิดกับใบที่ settle แล้ว การเพิ่มสถานะ = ถอยออกจากสถานะปลายทาง

ทุกใบยัง bump version เสมอ

attribute-change ก็ได้ version ใหม่ ⇒ version guard ของคุณจะไม่ทิ้งมัน · ถ้าไม่ bump คุณจะไม่มีทางรู้ว่ามี escalate เกิดขึ้น

ไม่มี event ชื่อ deposit.auto_approved และจะไม่มีวันมี

รายการที่ค้างคิวเจ้าหน้าที่นานเกินเกณฑ์ ไม่ถูกอนุมัติอัตโนมัติ — เราส่ง deposit.under_review ซ้ำ ด้วย review.escalated: true และ version ใหม่

ไม่มีเส้นทางไหนในระบบที่ timeout แล้วกลายเป็น settled


ตัวอย่างจริง 6 ใบ ​

ทั้ง 6 ใบเป็น JSON ที่ parse ได้จริง · เงินเป็น integer หน่วยสตางค์ · เวลาเป็น UTC ลงท้าย Z มิลลิวินาที 3 หลัก

1. deposit.matched — 2 legs ผ่านช่องทางสำรอง ​

ใบนี้คือใบเดียวที่มี QR · สังเกตว่า expires_at สั้นลง จากตอนสร้างรายการ

json
{
  "id": "evt_01K3M7YB4C9QDR2XKF0T8VN3PA",
  "object": "event",
  "type": "deposit.matched",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T04:10:52.104Z",
  "sequence": 88207,
  "account": {
    "merchant_id": "mch_01K2G4H9XQ8M3TVB7NDZP0RCEF",
    "white_label_id": "wl_01K1QZ2M6JH4XR8TVN0BDC5APG"
  },
  "request": {
    "id": "req_01K3M7YA9C2NVRT4HB0XKDF8QS",
    "idempotency_key": "order-9981-topup"
  },
  "data": {
    "object": {
      "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
      "object": "deposit_order",
      "version": 3,
      "status": "awaiting_payment",
      "is_terminal": false,
      "merchant_reference": "ORD-9981",
      "amount_minor": 500000,
      "currency": "THB",
      "created_at": "2026-08-21T04:10:47.902Z",
      "expires_at": "2026-08-21T04:14:41.000Z",
      "checkout_url": "https://pay.example-wl.com/c/9f2c1ab6e0d74f",
      "liquidity_source": "external_gateway",
      "leg_count": 2,
      "legs": [
        {
          "leg_ref": "leg_7QF2H8KM",
          "sequence": 1,
          "amount_minor": 300000,
          "expires_at": "2026-08-21T04:14:41.000Z",
          "leg_status": "pending",
          "method": "promptpay_qr",
          "qr_payload": "00020101021229370016A00000067701011201150105560012345530376454063000.005802TH6304A1B2",
          "qr_image_url": "https://pay.example-wl.com/qr/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/1.png",
          "receiver": {
            "bank_code": "014",
            "bank_name_th": "ธนาคารไทยพาณิชย์",
            "account_number_masked": "xxx-x-x1234",
            "account_name_masked": "ส*** ใ***"
          }
        },
        {
          "leg_ref": "leg_7QF2H8KN",
          "sequence": 2,
          "amount_minor": 200000,
          "expires_at": "2026-08-21T04:14:41.000Z",
          "leg_status": "pending",
          "method": "promptpay_qr",
          "qr_payload": "00020101021229370016A00000067701011201150105560099887530376454062000.005802TH6304C3D4",
          "qr_image_url": "https://pay.example-wl.com/qr/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/2.png",
          "receiver": {
            "bank_code": "004",
            "bank_name_th": "ธนาคารกสิกรไทย",
            "account_number_masked": "xxx-x-x9887",
            "account_name_masked": "ว*** ป***"
          }
        }
      ],
      "sender": {
        "bank_code": "002",
        "account_number_masked": "xxx-x-x5567",
        "account_name_th_masked": "ป*** ส***"
      },
      "metadata": { "user_id": "u_88213", "order_id": "ORD-9981" }
    },
    "previous_attributes": {
      "version": 2,
      "status": "awaiting_liquidity",
      "legs": null,
      "liquidity_source": null,
      "leg_count": null,
      "expires_at": "2026-08-21T04:30:47.902Z"
    }
  }
}

สิ่งที่ต้องทำกับใบนี้

  • แสดง ทุก leg — legs.length > 1 ได้ ⇒ UI ต้องเป็น "ขั้นที่ 1 จาก 2" ไม่ใช่หยิบ legs[0] มาแสดงใบเดียว
  • 🔴 นับถอยหลังจาก expires_at ที่ได้มาในใบนี้ ห้าม hardcode · ค่าเดิมคือ 20 นาที ค่าใหม่คือ ~4 นาที
  • liquidity_source: "external_gateway" เป็นข้อมูลประกอบเท่านั้น 🔴 ห้ามใช้ตัดสินใจเครดิต
  • 🔴 qr_payload ห้าม log ห้ามเก็บลง DB ห้ามส่งต่อ — ใช้แสดงผลแล้วทิ้ง · ทางที่ปลอดภัยกว่าคือส่งผู้ใช้ไปที่ checkout_url

2. deposit.under_review — ธนาคารปิดตรวจ + ชื่อผู้โอนไม่ตรง ​

json
{
  "id": "evt_01K3M9F2XN8VQKR4T0BDHY7WZE",
  "object": "event",
  "type": "deposit.under_review",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T04:19:08.774Z",
  "sequence": 88231,
  "account": {
    "merchant_id": "mch_01K2G4H9XQ8M3TVB7NDZP0RCEF",
    "white_label_id": "wl_01K1QZ2M6JH4XR8TVN0BDC5APG"
  },
  "data": {
    "object": {
      "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
      "object": "deposit_order",
      "version": 6,
      "status": "under_review",
      "is_terminal": false,
      "merchant_reference": "ORD-9981",
      "amount_minor": 500000,
      "currency": "THB",
      "review": {
        "opened_at": "2026-08-21T04:19:08.601Z",
        "escalated": false,
        "four_eyes_required": false,
        "estimated_resolution_bucket": "under_1h",
        "reasons": [
          {
            "code": "BANK_DISABLED",
            "display_th": "ขณะนี้ระบบตรวจสอบสลิปของธนาคารนี้ปิดรับชั่วคราว เจ้าหน้าที่กำลังตรวจสอบรายการของคุณด้วยตนเอง เงินของคุณไม่หาย ไม่ต้องโอนซ้ำ",
            "display_en": "Automatic slip checking for this bank is temporarily unavailable. Our team is reviewing your transfer manually.",
            "actionable_by": "support",
            "detail": { "bank_code": "002" }
          },
          {
            "code": "SENDER_NAME_MISMATCH",
            "display_th": "ชื่อบัญชีผู้โอนในสลิปไม่ตรงกับชื่อที่แจ้งไว้ตอนสร้างรายการ เงินของคุณไม่หาย ไม่ต้องโอนซ้ำ",
            "display_en": "The sender name on the slip does not match the name registered for this order.",
            "actionable_by": "end_user",
            "detail": { "registered_name_masked": "ป*** ส***", "matched": false }
          }
        ]
      },
      "slip": { "received_at": "2026-08-21T04:19:02.180Z", "is_late": false },
      "verification": {
        "amount_exact": true,
        "within_time_window": true,
        "sender_bank_match": true,
        "sender_account_match": true,
        "sender_name_match": false,
        "receiver_match": true
      },
      "metadata": { "user_id": "u_88213", "order_id": "ORD-9981" }
    },
    "previous_attributes": {
      "version": 5,
      "status": "slip_processing",
      "review": null,
      "verification": null
    }
  }
}

สิ่งที่ต้องทำกับใบนี้

  • แสดง review.reasons[0].display_th ตรง ๆ — เราเขียนข้อความมาให้แล้ว คุณไม่ต้องเขียนเอง และ reasons[] เรียงตามความสำคัญ ตัวแรกคือตัวที่ควรแสดงถ้าจอมีที่ว่างบรรทัดเดียว
  • 🔴 ห้ามปล่อยของ · ห้ามมีปุ่ม "โอนใหม่" — เงินโอนเข้าไปแล้ว การให้โอนซ้ำคือทำให้ผู้ใช้เสียเงินสองรอบ
  • verification{} เป็น boolean ล้วน 6 ตัว — เราไม่ส่งชื่อ/เลขบัญชีที่อ่านได้จากสลิปออกมา เพราะคนโอนจริงอาจเป็นคนละคนกับที่คุณแจ้ง (โอนจากบัญชีภรรยา) = ข้อมูลส่วนบุคคลของบุคคลที่สาม
  • estimated_resolution_bucket ∈ under_15m | under_1h | under_4h | extended เอาไปบอกผู้ใช้ได้เลยว่าประมาณเท่าไร

3. deposit.settled — ยอดโอนจริงกับยอดรับเข้าต้องแยกกัน ​

json
{
  "id": "evt_01K3M9ZZ2P7QW2NHT0RXKD5V8C",
  "object": "event",
  "type": "deposit.settled",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T04:21:11.402Z",
  "sequence": 88240,
  "account": {
    "merchant_id": "mch_01K2G4H9XQ8M3TVB7NDZP0RCEF",
    "white_label_id": "wl_01K1QZ2M6JH4XR8TVN0BDC5APG"
  },
  "data": {
    "object": {
      "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
      "object": "deposit_order",
      "version": 9,
      "status": "settled",
      "is_terminal": false,
      "merchant_reference": "ORD-9981",
      "amount_minor": 500000,
      "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,
      "overpayment": null,
      "currency": "THB",
      "created_at": "2026-08-21T04:10:47.902Z",
      "partial": false,
      "settled_via": "review",
      "liquidity_source": "external_gateway",
      "balance_transaction_id": "btxn_01K3M9ZZ4P7QW2NHT0RXKD5V8C",
      "settled_at": "2026-08-21T04:21:11.208Z",
      "clearing_available_at": "2026-08-24T04:21:11.208Z",
      "metadata": { "user_id": "u_88213", "order_id": "ORD-9981" }
    },
    "previous_attributes": { "version": 6, "status": "under_review" }
  }
}

ตัวเลขเหล่านี้อย่าสลับกัน

  • transferred_amount_minor → 500000 คือยอดธนาคารยืนยันของ settlement evidence; สูงกว่า A เฉพาะ same-slip overpay · transfer เพิ่มหลัง settle ไม่ทำให้ค่านี้กลายเป็นยอดสะสม
  • เครดิตผู้ใช้ปลายทาง = amount_received_minor → 500000 (5,000.00 บาท) — ยอดที่ระบบรับเข้า order
  • กระเป๋า merchant ขยับด้วย credited_amount_minor → 492500 (4,925.00 บาท)
  • fee_total_minor → 7500 · ต้องเป็นจริงเสมอว่า amount_received_minor − fee_total_minor = credited_amount_minor ⇒ ใส่ assert ข้อนี้ใน reconcile job รายวัน

ใช้ credited_amount_minor เครดิตผู้ใช้ = ผู้ใช้ได้เงินขาดเท่ากับค่าธรรมเนียมทุกออร์เดอร์

ถ้าเป็น same-slip overpay transferred_amount_minor จะมากกว่า amount_received_minor=ordered_amount_minor และ object เดียวกันมี overpayment{claim_id,cause:'same_transfer_excess',amount_minor,action_required:'contact_support'}. ยังต้องเครดิตครั้งเดียวด้วย amount_received_minor; แสดง claim ให้ผู้ใช้ติดต่อ support

สิ่งอื่นที่ต้องรู้จากใบนี้

  • settled_via: "review" แปลว่ารายการนี้ผ่านมือเจ้าหน้าที่ ไม่ใช่ auto — เก็บไว้ดูสถิติได้ แต่ไม่ต้องแสดงผู้ใช้
  • clearing_available_at คือเวลาที่ยอดนี้จะ ถอนได้ (T+72h) · ก่อนหน้านั้นยอดอยู่ใน pending_clearing ⇒ สร้างรายการถอนแล้วจะได้ 402 insufficient_balance ทั้งที่ dashboard เห็นตัวเลข
  • 🔴 is_terminal: false เพราะ settled ยังออกไป reversed ได้ · 🔴 อย่าใช้ is_terminal ตัดสินว่าเครดิตได้หรือไม่ — เงื่อนไขคือ status === 'settled' เท่านั้น
  • หลังใบนี้จะมี balance.credited ตามมา (bucket ปลายทางคือ pending_clearing) และอีก 72 ชม. จะมี balance.cleared

4. deposit.canceled — ระบบหมดทางจัดหาช่องทางให้โอน ​

เคสนี้เจ้าอื่นมักส่งเป็น expired หรือ HTTP 503 · ของเราสร้างรายการให้เสมอ แล้วปิดด้วย canceled

json
{
  "id": "evt_01K3N2QK8M4TVR7B0HDZX2C5NP",
  "object": "event",
  "type": "deposit.canceled",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T05:02:47.559Z",
  "sequence": 88318,
  "account": {
    "merchant_id": "mch_01K2G4H9XQ8M3TVB7NDZP0RCEF",
    "white_label_id": "wl_01K1QZ2M6JH4XR8TVN0BDC5APG"
  },
  "data": {
    "object": {
      "id": "dep_01K3N2QG5W8VRT0BHKZQ7M4YXD",
      "object": "deposit_order",
      "version": 4,
      "status": "canceled",
      "is_terminal": true,
      "merchant_reference": "ORD-10042",
      "amount_minor": 1750000,
      "currency": "THB",
      "created_at": "2026-08-21T05:01:22.117Z",
      "canceled_by": "system",
      "cancel_reason_code": "no_liquidity_available",
      "late_slip_accepted_until": null,
      "metadata": { "user_id": "u_90117", "order_id": "ORD-10042" }
    },
    "previous_attributes": { "version": 3, "status": "awaiting_liquidity" }
  }
}

canceled ไม่ได้แปลว่าจบเสมอไป — ต้องดู 2 field

ใบนี้จบจริง เพราะ ไม่เคยมีปลายทางให้ผู้ใช้โอน ⇒ is_terminal: true และ late_slip_accepted_until: null

แต่ใบที่ผู้ใช้เห็น QR แล้วกดยกเลิก แล้วเผลอโอนตามไป (เกิดสัปดาห์ละหลายครั้ง) จะมาแบบ is_terminal: false พร้อม late_slip_accepted_until เป็นเวลาจริง ⇒ ต้องเหลือปุ่มอัปสลิปไว้ ไม่ใช่ปิดออร์เดอร์

โค้ดที่เขียน case 'deposit.canceled': failOrder() จะทำให้ผู้ใช้เสียเงินจริง — เช็ค is_terminal ไม่ใช่ชื่อสถานะ

กติกาเดียวกันนี้ใช้กับ deposit.expired ทุกประการ

5. withdrawal.filled — ก้อนที่ 1 จากหลายก้อน ​

รายการถอน 10,000 บาทใบนี้ได้รับเงินก้อนแรก 4,000 บาท · ยังเหลืออีก 6,000

json
{
  "id": "evt_01K3NB4T7QW2VRH0DZXK5M8CNP",
  "object": "event",
  "type": "withdrawal.filled",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T06:48:03.221Z",
  "sequence": 88477,
  "account": {
    "merchant_id": "mch_01K2G4H9XQ8M3TVB7NDZP0RCEF",
    "white_label_id": "wl_01K1QZ2M6JH4XR8TVN0BDC5APG"
  },
  "data": {
    "object": {
      "id": "wdr_01K3NAX2R7T0QWVB5HDZKM9C4E",
      "object": "withdrawal_order",
      "version": 7,
      "status": "partially_filled",
      "merchant_reference": "WD-4471",
      "amount_total_minor": 1000000,
      "fee_total_minor": 3000,
      "fee_recognized_minor": 1200,
      "filled_minor": 400000,
      "remaining_minor": 600000,
      "reserved_minor": 0,
      "refunded_minor": 0,
      "settled_splits": 1,
      "max_splits": 4,
      "currency": "THB",
      "expires_at": "2026-08-21T10:44:58.000Z",
      "metadata": { "user_id": "u_77320", "payout_id": "WD-4471" }
    },
    "fill": {
      "leg_ref": "leg_5TQ0WH2E",
      "sequence": 1,
      "amount_minor": 400000,
      "fee_recognized_minor": 1200,
      "settled_at": "2026-08-21T06:48:03.004Z",
      "balance_transaction_id": "btxn_01K3NB4T5RW2VRH0DZXK5M8CNP",
      "auto_confirm_at": "2026-08-24T06:48:03.004Z"
    },
    "previous_attributes": {
      "version": 6,
      "status": "open",
      "filled_minor": 0,
      "remaining_minor": 1000000,
      "fee_recognized_minor": 0,
      "settled_splits": 0
    }
  }
}

1 รายการถอน = webhook หลายใบ

ถอน 10,000 อาจได้ withdrawal.matched ×3 + withdrawal.filled ×3 + withdrawal.settled ×1 = 7 ใบต่อ 1 รายการ

  • 🔴 withdrawal.filled ไม่ใช่จุดจบ — ปิดรายการเมื่อ status = 'settled' และ remaining_minor = 0 เท่านั้น
  • 🔴 อ่านยอดจาก data.object.filled_minor / remaining_minor ที่ผ่าน version guard · ห้าม SUM() ตาราง fill ของตัวเอง (event อาจยังมาไม่ครบ ⇒ ได้ค่าต่ำกว่าจริง) และ ห้าม += (delivery ซ้ำจะบวกเกิน)
  • ตาราง fill ฝั่งคุณมี leg_ref เป็น primary key ไว้ทำ timeline/audit เท่านั้น ห้ามเอาไปขับ business logic
  • 🔴 อย่าคำนวณค่าธรรมเนียมเองต่อก้อน — ค่าธรรมเนียมแบบคงที่ผูกกับ ใบ ไม่ใช่ต่อก้อน ⇒ ซอย 4 ครั้งไม่ได้จ่ายคงที่ 4 เท่า · ใช้ fee_recognized_minor ที่เราส่งมา
  • UI ต้องเป็น timeline หลายบรรทัด ไม่ใช่แถบสถานะเดียว

auto_confirm_at คือเวลาที่ก้อนนี้จะถือว่าผู้รับยืนยันแล้วโดยอัตโนมัติถ้าไม่มีใครทักท้วง (72 ชม.) · ก่อนถึงเวลานั้นอาจมี withdrawal.disputed เข้ามาได้

6. balance.debited — reversal · ใบที่ปิดไม่ได้ ​

json
{
  "id": "evt_01K3PB6QW2ZR9YT5N4KDXCVH8M",
  "object": "event",
  "type": "balance.debited",
  "api_version": "2026-08-01",
  "livemode": true,
  "created_at": "2026-08-21T09:41:20.006Z",
  "sequence": 89402,
  "account": {
    "merchant_id": "mch_01K2G4H9XQ8M3TVB7NDZP0RCEF",
    "white_label_id": "wl_01K1QZ2M6JH4XR8TVN0BDC5APG"
  },
  "data": {
    "object": {
      "id": "btxn_01K3PB6QW1M8HN0DZ2XKQ7RV4T",
      "object": "balance_transaction",
      "type": "reversal",
      "direction": "debit",
      "amount_minor": 492500,
      "currency": "THB",
      "bucket_from": "pending_clearing",
      "bucket_to": null,
      "balance_after": {
        "available_minor": 1284300,
        "pending_clearing_minor": 0,
        "reserved_minor": 200000
      },
      "source": { "type": "deposit", "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B" },
      "reverses_balance_transaction_id": "btxn_01K3M9ZZ4P7QW2NHT0RXKD5V8C",
      "reason_code": "SLIP_INVALIDATED_AFTER_SETTLEMENT",
      "reason_display_th": "รายการถูกยกเลิกหลังจากตรวจพบว่าสลิปไม่ถูกต้อง",
      "created_at": "2026-08-21T09:41:19.884Z"
    }
  }
}

สิ่งที่ต่างจากใบอื่นทั้งหมด

  • ไม่มี version ไม่มี previous_attributes — balance_transaction เป็น immutable ⇒ dedupe ด้วย id แล้ว สะสม ห้ามเขียนทับ
  • reverses_balance_transaction_id ชี้กลับไปที่ balance_transaction_id ของ deposit.settled ใบเดิม (ตัวอย่างที่ 3) ⇒ คุณกลับรายการฝั่งตัวเองได้แม่นยำ ไม่ต้องเดา · ระบบบังคับว่า reversal เกิดได้ ครั้งเดียวต่อ 1 รายการ
  • ยอด 492500 คือ credited_amount_minor เดิม (หลังหักค่าธรรมเนียม) · ค่าธรรมเนียมที่รับรู้ไปแล้วมี balance_transaction คืนกลับแยกใบ (type: "fee")
  • bucket_from: "pending_clearing" — การกลับรายการภายใน 72 ชม. ตัดจากยอดที่ยังรอเคลียร์ ไม่ใช่จากยอดที่ถอนได้ ⇒ กระเป๋าคุณไม่ติดลบ นี่คือเหตุผลทั้งหมดของการมี bucket รอเคลียร์
  • 🔴 ห้ามใช้ balance_after เป็นยอดคงเหลือของระบบคุณ — เป็น snapshot ณ ตอนนั้น ถ้า event สลับลำดับคุณจะเขียนทับด้วยยอดเก่า · ใช้เพื่อ ตรวจทาน เท่านั้น ยอดจริงดึงจาก GET /v1/pay/balance

พจนานุกรม reason code ​

ทุก entry ใน review.reasons[] หน้าตาแบบนี้:

json
{
  "code": "SENDER_NAME_MISMATCH",
  "display_th": "ชื่อบัญชีผู้โอนในสลิปไม่ตรงกับที่แจ้งไว้ตอนสร้างรายการ",
  "display_en": "The sender name on the slip does not match the one registered for this order.",
  "actionable_by": "end_user",
  "detail": { "registered_name_masked": "สม*** ใจ***", "matched": false }
}

เราส่งข้อความมาให้ — คุณไม่ต้องเขียนเอง

display_th และ display_en มาในทุกใบ · แสดง reasons[0].display_th ได้เลย

การเขียนข้อความเองจาก code ทำให้ข้อความในระบบคุณล้าสมัยเมื่อเราปรับถ้อยคำ และ code เป็น enum เปิด ⇒ วันหนึ่งคุณจะเจอ code ที่ map ไม่ได้แล้วจอว่าง

codeเกิดจากactionable_bydetail ที่จะได้
AMOUNT_MISMATCHV<X หรือ V>X และมีเหตุ review อื่นร่วมด้วยเท่านั้น; amount-only mismatch settle อัตโนมัติend_userexpected_minor, observed_minor
TIME_WINDOWเวลาบนสลิปอยู่นอกหน้าต่างของรายการsupportwindow_start, window_end, slip_time
SENDER_BANK_MISMATCHธนาคารผู้โอนไม่ตรงกับที่แจ้งend_userregistered_bank_code
SENDER_ACCOUNT_MISMATCHเลขบัญชีผู้โอนไม่ตรงกับที่แจ้งend_userregistered_account_masked
SENDER_NAME_MISMATCHชื่อผู้โอนไม่ตรงกับที่แจ้งend_userregistered_name_masked, matched
RECEIVER_MISMATCHโอนไปบัญชีปลายทางผิดend_user🔴 ไม่มี detail เลย
BANK_DISABLEDระบบตรวจสลิปของธนาคารนั้นปิดชั่วคราวsupportbank_code (ของผู้โอนเอง)
DUPLICATE_SLIPสลิปใบนี้เคยถูกใช้ไปแล้วend_user—
SLIP_INVALIDระบบตรวจสลิปตอบว่าสลิปใช้ไม่ได้support—
SLIP_UNREADABLEรูปอ่านไม่ออกตั้งแต่ก่อนตรวจ 6 มิติend_user— (มาใน deposit.slip_rejected ไม่ใช่ under_review)
AMBIGUOUS_MATCHจับคู่ได้มากกว่า 1 รายการsupport—
PROVIDER_UNAVAILABLEระบบตรวจสลิปใช้งานไม่ได้ชั่วคราวsupport—
SENDER_ACCOUNT_UNAVAILABLEระบบตรวจสลิปไม่คืนข้อมูลบัญชีผู้โอนsupport—
LATE_SLIPสลิปมาถึงหลังรายการหมดอายุsupportoriginal_expires_at
CANCELED_ORDER_SLIPสลิปของรายการที่ถูกยกเลิกไปแล้วsupportcanceled_at
ALLOCATION_CONFLICTยอดส่วนนั้นถูกจับคู่ให้รายการอื่นไปแล้วsupportdispute_id
SETTLE_CONFLICTขั้นตอนเครดิตค้างหรือล้มเหลวเกิน 15 นาทีsupport—
ROUTE_VERIFICATION_PENDINGช่องทางสำรองต้องตรวจเพิ่มsupport—
LATE_ROUTE_CONFIRMATIONช่องทางสำรองยืนยันหลังปิดรายการไปแล้วsupport—
RISK_HOLDระบบตรวจความเสี่ยงกันไว้support—
NO_SLIP_IMAGEผู้ใช้ส่งเลขอ้างอิงจากแอปธนาคารแทนรูปสลิป ⇒ บังคับเข้าคิวเจ้าหน้าที่เสมอ——

กฎที่ต้องรู้เกี่ยวกับ detail ​

  • 🔴 RECEIVER_MISMATCH ไม่มี detail เด็ดขาด — บัญชีปลายทางเป็นของอีกฝ่าย การบอกว่า "ที่ถูกคือ xxx1234" คือการยืนยันบัญชีของคนอื่นให้คนที่โอนผิดรู้
  • detail ที่มีข้อมูลส่วนบุคคลได้ เฉพาะค่าที่คุณส่งมาเอง ตอนสร้างรายการ (sender.*) และ echo กลับแบบ mask
  • 🔴 ค่าที่อ่านได้จากสลิปจริงไม่มีวันส่งออก — มีแค่ matched: false

actionable_by เอาไปทำอะไร ​

ค่าแปลว่าUI ควรเป็น
end_userผู้ใช้ทำอะไรบางอย่างได้ (แนบหลักฐานเพิ่ม เปลี่ยนบัญชีผู้โอนครั้งหน้า)แสดงข้อความ + ทางเลือกที่ทำได้ · 🔴 ยังห้ามมีปุ่ม "โอนใหม่"
supportรอเจ้าหน้าที่อย่างเดียว ผู้ใช้ทำอะไรไม่ได้แสดงข้อความ + เวลาโดยประมาณจาก estimated_resolution_bucket

code ของการปฏิเสธและการกลับรายการ ​

rejection.code (ใน deposit.rejected / withdrawal.rejected) เป็น enum เปิดเช่นกัน · มาพร้อม display_th / display_en เสมอ

reversal.reason_code ชุดปัจจุบัน — เปิด เช่นกัน:

BANK_RECALL · ACCOUNT_FROZEN · SLIP_INVALIDATED_AFTER_SETTLEMENT · DUPLICATE_CREDIT · OPS_ERROR · MERCHANT_DISPUTE_LOST

withdrawal.fill_reversed ใช้ได้เฉพาะสามเหตุผล true transfer failure ชุดแรกเท่านั้น ส่วน DUPLICATE_CREDIT · OPS_ERROR · MERCHANT_DISPUTE_LOST เป็น deposit-only correction: ส่ง deposit.reversed + balance.debited แต่ ไม่ส่ง withdrawal.fill_reversed และไม่เปลี่ยนยอดรวม fill หรือ fee ฝั่งถอน

cancel_reason_code ชุดปัจจุบัน — เปิด:

no_liquidity_available · merchant_request · end_user_request · approval_ttl_expired

ขั้นต่อไป ​

อ่านต่อเมื่อ
ลำดับไม่การันตี — version guard🔴 ก่อนเขียน switch ตัวแรก
ตรวจลายเซ็น Ed25519ก่อนเชื่อ payload ใบไหนก็ตาม
Retry และ endpoint ล่มอยากรู้ว่าตอบ non-2xx แล้วจะเกิดอะไรต่อ
สถานะรายการฝากต้องการ map สถานะ → หน้าจอผู้ใช้
รับ fill หลายก้อนทำฝั่งถอน
พจนานุกรม fieldไม่แน่ใจว่าชื่อ field ไหนคือตัวจริง

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