Skip to content

สถานะรายการฝาก ​

กฎ 3 ข้อที่ต้องรู้ก่อน ​

  1. status = "settled" เท่านั้นที่แปลว่าเครดิตได้ — ทุกค่าอื่นห้ามปล่อยของ ไม่มีข้อยกเว้น
  2. expired และ canceled ไม่ใช่จุดจบเสมอไป — ดูที่ is_terminal ไม่ใช่ที่ชื่อสถานะ
  3. webhook เป็นหลัก · GET เป็นตัวสำรอง — ไม่ใช่ทางกลับกัน

ทั้ง 9 สถานะ ​

statusความหมายจบแล้ว💰 เงินอยู่ไหนคุณต้องทำอะไร
awaiting_liquidityสร้างรายการแล้ว แต่ยังไม่มีปลายทางให้โอน ไม่มี QR ไม่มีเลขบัญชีไม่ยังไม่โอนหน้าจอ "กำลังจัดหาช่องทางโอน" + เสนอ liquidity.suggested_amounts_minor[] · 🔴 ห้ามแสดง QR ห้ามบอกให้โอน
awaiting_paymentจับคู่แล้ว มี legs[] และ expires_atไม่ยังไม่โอน (อยู่ที่ผู้ใช้)แสดง QR/เลขบัญชี + countdown จาก expires_at + กล่อง "โอนจากบัญชีนี้เท่านั้น"
slip_processingรับสลิปแล้ว กำลังตรวจไม่โอนแล้ว รอตรวจ"กำลังตรวจสอบสลิป" · disable ปุ่มอัปสลิปซ้ำ · 🔴 ห้ามปล่อยของ
under_reviewมิติ non-amount ไม่ผ่าน / ติด policy / ธนาคารปิดตรวจอัตโนมัติ / สลิปซ้ำ / กำกวม; amount mismatch อย่างเดียวไม่เข้า state นี้ไม่โอนแล้ว รอเจ้าหน้าที่แสดง review.reasons[0].display_th ตรง ๆ · สีเหลือง/ฟ้า ไม่ใช่แดง · 🔴 ห้ามปล่อยของ ห้ามมีปุ่ม "โอนใหม่"
settledเครดิตแล้ว; clean short-pay มี partial:true, settled_via:'auto'; อาจมี overpayment{} flagเกือบ (ออกได้ทางเดียวคือ reversed)รับเข้าเฉพาะ amount_received_minor; ส่วนเกินอยู่ใน claimเครดิตผู้ใช้ด้วย amount_received_minor; short-pay แสดงยอดจริงที่รับ; ถ้ามี flag ให้แสดงยอดเกิน/claim และปุ่มติดต่อ support
expiredหมด expires_at โดยไม่มีสลิป · 🔴 ไม่ใช่การปฏิเสธเงินไม่ จนพ้นหน้าต่างย้อนหลังปกติยังไม่โอน · ถ้าอัปสลิปย้อนหลัง = โอนแล้วต้องเหลือปุ่ม "ฉันโอนไปแล้วแต่หมดเวลา" จนถึง late_slip_accepted_until
canceledปิดก่อนโอน — คุณกด / ผู้ใช้กด / ระบบปิดเพราะหาปลายทางไม่ได้ขึ้นกับ late_slip_accepted_untilปกติยังไม่โอนถ้ามี late_slip_accepted_until ต้องเหลือปุ่มอัปสลิป เหมือน expired
rejectedเจ้าหน้าที่ตัดสินไม่รับสลิปใบนี้✅ไม่เครดิตแสดง rejection.display_th + แสดง deposit_id ให้ copy ได้
reversedเคย settled แล้วถูกกลับรายการ✅หักคืนแล้ว🔴 ต้องหักยอดผู้ใช้คืน และแจ้งผู้ใช้เสมอ · handler ของ deposit.reversed + balance.debited ปิดไม่ได้

rejected ไม่เท่ากับคืนเงิน

rejected แปลว่า "สลิปใบนี้ไม่ใช่หลักฐานของรายการนี้" ถ้าเงินจริงเข้าไปแล้วแต่เป็นของรายการอื่น เรื่องนั้นเดินต่อในเส้น claim ไม่ใช่ที่สถานะนี้ · อย่าเขียนโค้ดที่ auto-refund เมื่อเห็น rejected


is_terminal ไม่ใช่ชื่อสถานะ ​

js
// ❌ ผิด — ปิดออร์เดอร์ทั้งที่ผู้ใช้อาจโอนไปแล้วและกำลังจะอัปสลิป
if (['expired', 'canceled', 'rejected', 'reversed'].includes(d.status)) closeOrder()

// ✅ ถูก
if (d.is_terminal) closeOrder()

expired และ canceled ที่มี late_slip_accepted_until จะมี is_terminal: false · เมื่อหน้าต่างปิดจริง เราจะยิง deposit.expired / deposit.canceled ซ้ำอีกครั้ง ด้วย is_terminal: true และ version ใหม่ — นั่นคือจุดที่ปิดออร์เดอร์ได้


อ่านสถานะรายการเดียว ​

bash
curl https://api.example-wl.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "PG-Timestamp: 1755749900" \
  -H "PG-Nonce: 01JQA5TB4N6Q8S0U2W4Y6A8C0E" \
  -H "PG-Version: 2026-08-01" \
  -H "PG-Signature: v1=8e0b...cd72"

ตัวอย่าง response ของรายการที่เครดิตแล้ว

json
{
  "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
  "object": "deposit_order",
  "version": 7,
  "status": "settled",
  "is_terminal": false,
  "merchant_reference": "ORD-9981",
  "amount_minor": 500000,
  "ordered_amount_minor": 500000,
  "currency": "THB",
  "created_at": "2026-08-21T04:10:47.902Z",
  "settled_at": "2026-08-21T04:19:33.204Z",
  "settled_via": "auto",
  "partial": false,
  "liquidity_source": "internal",
  "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": 2000 },
    { "component": "vat", "amount_minor": 500 }
  ],
  "credited_amount_minor": 492500,
  "overpayment": null,
  "balance_transaction_id": "btxn_01K3M9ZZ4P7QW2NHT0RXKD5V8C",
  "clearing_available_at": "2026-08-24T04:19:33.204Z",
  "sender": {
    "bank_code": "004",
    "account_number_masked": "xxx-x-x7890",
    "account_name_th_masked": "ส*** ใ***"
  },
  "metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
}

ยอดโอนจริงกับยอดรับเข้าคนละความหมาย

fieldใช้ทำอะไร
transferred_amount_minorยอด settlement evidence ที่ธนาคารยืนยัน; same-slip overpay อาจสูงกว่า A · transfer เพิ่มหลัง settle ไม่บวกสะสมใน field นี้
amount_received_minorเครดิตให้ผู้ใช้ปลายทาง — ยอดที่ระบบรับเข้า order นี้
fee_total_minorค่าธรรมเนียมที่เก็บจากคุณ
credited_amount_minorยอดที่เข้ากระเป๋าของคุณ = amount_received_minor − fee_total_minor

ใช้ credited_amount_minor ไปเครดิตผู้ใช้ = ผู้ใช้ได้เงินขาดเท่ากับค่าธรรมเนียมทุกรายการ · ใช้ amount_received_minor ไปกระทบยอดกระเป๋า = ยอดในระบบคุณสูงกว่าจริงเท่ากับค่าธรรมเนียมทั้งหมด

เก็บทุกตัวแยกคอลัมน์ แล้ว assert amount_received_minor = min(transferred_amount_minor, ordered_amount_minor), 0 ≤ fee_total_minor ≤ amount_received_minor และ amount_received_minor − fee_total_minor = credited_amount_minor

ถ้า same-slip transferred_amount_minor > amount_received_minor รายการยัง settled; object จะมี overpayment{cause:'same_transfer_excess',claim_id,amount_minor,action_required:'contact_support'}. เครดิตเฉพาะ amount_received_minor และให้ผู้ใช้ติดต่อ support พร้อม claim id · ถ้ามี transfer เพิ่มหลัง settle จะได้ cause:'additional_transfer' โดย transferred_amount_minor ยังคงเป็น evidence เดิม ไม่ใช่ยอดสะสม

credited_amount_minor ยังถอนไม่ได้ทันที

ยอดที่เพิ่งเข้าจะอยู่ในกลุ่ม pending_clearing ก่อน แล้วย้ายไป available ที่เวลา clearing_available_at (T+72 ชม.) · สร้างรายการถอนด้วยยอดที่ยังไม่ clear จะได้ 402 insufficient_balance ทั้งที่ dashboard เห็นเงิน


อ่านหลายรายการทีเดียว ​

bash
curl -G https://api.example-wl.com/v1/pay/deposits \
  --data-urlencode "status=settled" \
  --data-urlencode "updated_after=2026-08-21T00:00:00.000Z" \
  --data-urlencode "limit=100" \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "PG-Version: 2026-08-01" \
  -H "PG-Signature: v1=1d47...9f30"
json
{
  "data": [
    {
      "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B", "object": "deposit_order", "version": 7,
      "status": "settled", "is_terminal": false, "merchant_reference": "ORD-9981",
      "amount_minor": 500000, "ordered_amount_minor": 500000, "currency": "THB",
      "created_at": "2026-08-21T04:10:47.902Z", "transferred_amount_minor": 500000,
      "amount_received_minor": 500000, "fee_total_minor": 7500,
      "fee_breakdown": [{ "component": "processing_fee", "amount_minor": 7500 }],
      "credited_amount_minor": 492500, "partial": false, "settled_via": "auto",
      "liquidity_source": "internal", "overpayment": null,
      "balance_transaction_id": "btxn_01K3M9ZZ4P7QW2NHT0RXKD5V8C",
      "settled_at": "2026-08-21T04:19:33.204Z",
      "clearing_available_at": "2026-08-24T04:19:33.204Z"
    },
    {
      "id": "dep_01K3MB2K8T4WQZ6XR0NHD7YV5A", "object": "deposit_order", "version": 5,
      "status": "settled", "is_terminal": false, "merchant_reference": "ORD-9984",
      "amount_minor": 120000, "ordered_amount_minor": 120000, "currency": "THB",
      "created_at": "2026-08-21T04:35:10.128Z", "transferred_amount_minor": 120000,
      "amount_received_minor": 120000, "fee_total_minor": 3200,
      "fee_breakdown": [{ "component": "processing_fee", "amount_minor": 3200 }],
      "credited_amount_minor": 116800, "partial": false, "settled_via": "auto",
      "liquidity_source": "internal", "overpayment": null,
      "balance_transaction_id": "btxn_01K3MB2K9V5XR0Q7NHD8YWC6FA",
      "settled_at": "2026-08-21T04:41:02.771Z",
      "clearing_available_at": "2026-08-24T04:41:02.771Z"
    }
  ],
  "has_more": true,
  "next_cursor": "cur_01K3MB9Q0R2T4V6X8Z0B2D4F6H"
}
ข้อกติกา
limit1..200 — เปลี่ยนกลางคันได้
cursorค่า opaque จาก next_cursor
เปลี่ยน filter กลางคัน🔴 เปลี่ยน status / ช่วงเวลาแล้วใช้ cursor เดิม → 400 cursor_filter_mismatch ต้องเริ่มหน้าใหม่

อย่า poll ถี่ — ตัวเลขจริง ​

จำนวนที่ชน rate limit

GET /v1/pay/* ทั้งหมดใช้ bucket เดียวกันคือ 1,200 ครั้งต่อ 60 วินาที ต่อ API key (burst 2 เท่า)

วิธี pollรายการที่เปิดพร้อมกันrequest/นาทีผล
ทุก 1 วินาที รายตัว201,200🔴 ชนพอดี — รายการที่ 21 ทำให้ทั้ง key ติด 429
ทุก 1 วินาที รายตัว1006,000🔴 ชนตั้งแต่วินาทีที่ 12 ของทุกนาที
ทุก 5 วินาที รายตัว1001,200🔴 ชนพอดี ไม่มีที่ว่างให้ GET อื่นเลย
backoff 3 → 5 → 10 → 30 วินาที100~200–400✅ ปลอดภัย
GET /v1/pay/deposits?status= ทุก 15 วินาที (200 ใบ/ครั้ง)1,000+4✅ ปลอดภัยมาก

🔴 429 ทำให้ GET ทุกเส้นของ key นั้นติดไปด้วย ไม่ใช่เฉพาะรายการที่คุณ poll · และ retry เป็นลูปมี penalty ทวีคูณ

กติกาที่ปลอดภัย ​

  1. webhook เป็นช่องแจ้งเตือนหลัก ไม่ใช่ source of truth — สถานะ/ledger ที่อ่านผ่าน GET /v1/pay/* และงาน reconcile เป็นตัวตัดสินเมื่อข้อมูลไม่ตรงกัน
  2. poll รายตัวใช้ backoff 3 → 5 → 10 → 30 วินาที แล้วหยุดเมื่อ is_terminal เป็น true หรือ status = "settled"
  3. รายการที่ under_review ห้าม poll ต่อ — ใช้เวลาที่เจ้าหน้าที่ตรวจ ให้รอ webhook แทน ถ้าจะเช็คก็ใช้ batch วันละไม่กี่ครั้ง
  4. reconcile รายวันใช้ GET /v1/pay/events?since_sequence= + GET /v1/pay/deposits?updated_after= ไม่ใช่ไล่ GET รายตัวทั้งวัน
  5. เคารพ header ที่เราคืนมาทุก response ไม่ใช่เฉพาะตอน 429
RateLimit-Limit: 1200
RateLimit-Remaining: 1187
RateLimit-Reset: 41
RateLimit-Policy: 1200;w=60
js
// Node — poll แบบ backoff ที่หยุดถูกจุด
const BACKOFF_MS = [3000, 5000, 10000, 30000]

async function waitForDeposit(depositId, { maxMs = 15 * 60 * 1000 } = {}) {
  const deadline = Date.now() + maxMs
  let step = 0

  while (Date.now() < deadline) {
    const d = await getDeposit(depositId)

    if (d.status === 'settled') return d
    if (d.is_terminal) return d
    // รอเจ้าหน้าที่ตรวจ — ไม่มีประโยชน์ที่จะถามซ้ำถี่ ๆ ให้ webhook เป็นคนบอก
    if (d.status === 'under_review') return d

    await sleep(BACKOFF_MS[Math.min(step++, BACKOFF_MS.length - 1)])
  }
  return getDeposit(depositId)
}
php
<?php
// PHP — batch แทนการไล่ทีละใบ
function syncRecentDeposits(string $since, array $headers): int
{
    $cursor = null;
    $count  = 0;

    do {
        $query = http_build_query(array_filter([
            'updated_after' => $since,
            'limit'         => 200,
            'cursor'        => $cursor,
        ]));

        $res = httpGet("https://api.example-wl.com/v1/pay/deposits?{$query}", $headers);

        foreach ($res['data'] as $d) {
            // 🔴 เขียนทับเฉพาะเมื่อ version สูงกว่าที่มีอยู่ ไม่ใช่ทับทุกครั้ง
            applyDepositIfNewer($d['id'], $d['version'], $d);
            $count++;
        }

        $cursor = $res['has_more'] ? $res['next_cursor'] : null;
    } while ($cursor !== null);

    return $count;
}

GET แล้วได้ค่าเก่ากว่า webhook ได้ไหม ​

ได้ ถ้า webhook ของ version ใหม่มาถึงคุณก่อนที่คุณจะยิง GET แล้ว GET ไปเจอ replica ที่ตามหลังอยู่ · ทางแก้เหมือนกันทั้งสองทาง: เขียนทับสถานะเฉพาะเมื่อ version ที่ได้มาสูงกว่าที่เก็บไว้ ไม่ว่าค่านั้นจะมาจาก webhook หรือจาก GET

sql
UPDATE deposits
   SET status = $2, version = $3, payload = $4, updated_at = now()
 WHERE deposit_id = $1
   AND version < $3;      -- rowCount = 0 แปลว่าเรามีของใหม่กว่าอยู่แล้ว ให้ข้ามไปเลย

รายละเอียดเต็มอยู่ที่ ลำดับ webhook ไม่การันตี


ขั้นต่อไป ​

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