รายการฝากที่ยังค้างอยู่
"ค้าง" ในหน้านี้หมายถึงรายการที่ยังไม่จบและยังกินโควตาของผู้ใช้อยู่
status | ค้างในความหมายไหน | ผู้ใช้ทำอะไรได้ |
|---|---|---|
awaiting_liquidity | ยังหาปลายทางอยู่ | รอ หรือกดยกเลิก |
awaiting_payment | มี QR แล้ว รอผู้ใช้โอน | โอน หรือกดยกเลิก (ตราบใดที่ยังไม่มีสลิป) |
slip_processing | โอนแล้ว กำลังตรวจ | รอ — ยกเลิกไม่ได้แล้ว |
under_review | โอนแล้ว รอเจ้าหน้าที่ | รอ — ยกเลิกไม่ได้แล้ว |
expired / canceled ที่ is_terminal: false | ปิดไปแล้ว แต่ยังรับสลิปย้อนหลังได้ | อัปสลิปย้อนหลังจนถึง late_slip_accepted_until |
ทำไมต้องหาก่อนสร้างใบใหม่
ระบบมีเพดานรายการค้างต่อผู้ใช้ ถ้าเกินคุณจะสร้างรายการไม่ได้
ส่งอะไรมาใน client{} | นับกลุ่มยังไง | เพดาน |
|---|---|---|
ครบ (device_fingerprint + ip) | (merchant, end_user_ref, device_fingerprint) | 2 ใบ |
| ไม่ส่ง | (merchant, end_user_ref) เท่านั้น | 🔴 1 ใบ |
และมีกฎอีกข้อที่รัดกุมกว่านั้น: ห้ามมีรายการ awaiting_payment ที่ยอดเท่ากันและบัญชีผู้โอนเดียวกันค้างอยู่พร้อมกันสองใบ
กฎนี้มีไว้ป้องกันคุณ
ถ้าผู้ใช้เปิดสองรายการยอด 5,000 บาทเท่ากันจากบัญชีเดียวกัน แล้วโอนไปหนึ่งครั้ง ระบบจะไม่มีทางรู้เลยว่าสลิปใบนั้นเป็นของรายการไหน — เพราะทั้ง 6 มิติที่ใช้ตรวจตรงกันหมดทั้งสองใบ ⇒ ทั้งสองใบเข้าคิวเจ้าหน้าที่ (AMBIGUOUS_MATCH) เราจึงตัดปัญหาที่ต้นทางด้วยการไม่ให้สร้างใบที่สอง
วิธีที่ 1 — อ่านจาก 409 ตอนสร้าง (แม่นที่สุด)
เมื่อชนกฎ เราคืน id ของรายการที่ค้างอยู่มาให้เลย ไม่ต้องไปค้นเอง
409 duplicate_open_deposit
{
"error": {
"code": "duplicate_open_deposit",
"type": "invalid_request_error",
"message": "An open deposit with the same amount and sender already exists.",
"message_th": "มีรายการฝากยอดเท่ากันจากบัญชีผู้โอนเดียวกันค้างอยู่แล้ว",
"retryable": false,
"retry_after_seconds": null,
"request_id": "req_01JQA5TC6P8R0T2V4X6Z8B0D2F",
"details": {
"open_deposit_ids": ["dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B"]
}
}
}ต้องทำ: พาผู้ใช้กลับไปที่รายการเดิม อย่าสร้างใบใหม่ อย่าเปลี่ยนยอดให้เขาเองแบบเงียบ ๆ
409 too_many_open_deposits
{
"error": {
"code": "too_many_open_deposits",
"type": "invalid_request_error",
"message": "Too many unpaid deposits are open for this end user.",
"message_th": "มีรายการฝากที่ยังไม่ได้ชำระค้างอยู่หลายรายการ",
"retryable": false,
"retry_after_seconds": null,
"request_id": "req_01JQA5TD8Q0S2U4W6Y8A0C2E4G",
"details": {
"open_deposit_ids": [
"dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"dep_01K3M8B2VQ7HYD4NKR0XF9TW6E"
]
}
}
}ต้องทำ: แสดงรายการที่ค้างให้ผู้ใช้เห็น แล้วให้เขาเลือกว่าจะ "โอนต่อ" ใบไหน หรือจะยกเลิกใบไหน
// Node — จัดการทั้งสอง 409 ในที่เดียว
async function startTopup(order) {
try {
return await createDeposit(order)
} catch (err) {
if (err.code === 'duplicate_open_deposit' || err.code === 'too_many_open_deposits') {
const ids = err.details.open_deposit_ids ?? []
const open = await Promise.all(ids.map(getDeposit))
return { kind: 'resume', open } // ❌ ห้ามสร้างใบใหม่ ❌ ห้าม retry เงียบ ๆ
}
throw err
}
}<?php
// PHP — เวอร์ชันเดียวกัน
try {
return createDeposit($order, $headers);
} catch (ApiError $e) {
if (in_array($e->code, ['duplicate_open_deposit', 'too_many_open_deposits'], true)) {
$open = [];
foreach ($e->details['open_deposit_ids'] ?? [] as $id) {
$open[] = getDeposit($id, $headers);
}
return ['kind' => 'resume', 'open' => $open];
}
throw $e;
}หน้าจอที่ควรขึ้น
คุณมีรายการเติมเงินที่ยังไม่เสร็จ
5,000.00 บาท กสิกรไทย xxx-x-x1234 หมดเวลาใน 12:31
[ โอนต่อ ] [ ยกเลิกรายการนี้ ]
สร้างรายการใหม่ได้เมื่อปิดรายการเดิมแล้ววิธีที่ 2 — ไล่ดูรายการค้างทั้งหมดของคุณ
curl -G https://api.example-wl.com/v1/pay/deposits \
--data-urlencode "status=awaiting_payment" \
--data-urlencode "limit=200" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Version: 2026-08-01" \
-H "PG-Signature: v1=5c92...ae10"filter ที่สัญญากำหนดไว้สำหรับ endpoint นี้คือ status · updated_after · cursor · limit เท่านั้น
{
"data": [
{
"id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"object": "deposit_order",
"version": 2,
"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:30:47.902Z",
"sender": {
"bank_code": "004",
"account_number_masked": "xxx-x-x7890",
"account_name_th_masked": "ส*** ใ***"
},
"metadata": { "user_id": "u_88213", "order_id": "ORD-9981" }
}
],
"has_more": false,
"next_cursor": null
}วิธีที่ 3 — ดัชนีฝั่งคุณเอง (จำเป็น ไม่ใช่ทางเลือก)
การหารายการค้าง "ของผู้ใช้คนหนึ่ง" ต้องอาศัยดัชนีฝั่งคุณ
ชุด filter ของ GET /v1/pay/deposits ที่สัญญากำหนดไว้ไม่มีการค้นด้วย end_user_ref ⇒ คุณต้องเก็บ mapping end_user_ref → deposit_id ไว้เองตั้งแต่วินาทีที่สร้างรายการ ทั้งเส้น 201 และเส้น 202
นี่ไม่ใช่ภาระเกินจำเป็น คุณต้องเก็บ deposit_id ลง DB อยู่แล้วเพื่อ reconcile และเพื่อให้ support ตามเรื่องได้ · การมี index บน (end_user_ref, status) เพิ่มอีกชั้นเดียวทำให้หน้า "รายการค้างของฉัน" ทำงานได้โดยไม่ต้องยิง API เลย
-- ตารางฝั่ง merchant ที่ทำให้หน้ารายการค้างเร็วและไม่กิน rate limit
CREATE TABLE deposits (
deposit_id text PRIMARY KEY, -- 🔴 text ไม่ใช่ varchar(20) ห้าม parse prefix
merchant_reference text NOT NULL UNIQUE,
idempotency_key text NOT NULL UNIQUE,
end_user_ref text NOT NULL,
status text NOT NULL,
is_terminal boolean NOT NULL DEFAULT false,
version bigint NOT NULL,
amount_minor bigint NOT NULL,
expires_at timestamptz,
late_slip_accepted_until timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- หน้ารายการค้างของผู้ใช้คนหนึ่ง อ่านจากตารางนี้ ไม่ต้องยิง API
CREATE INDEX deposits_open_idx ON deposits (end_user_ref)
WHERE is_terminal = false;-- รายการที่ยังกินโควตาของผู้ใช้คนนี้
SELECT deposit_id, status, amount_minor, expires_at
FROM deposits
WHERE end_user_ref = $1
AND is_terminal = false
AND status IN ('awaiting_liquidity', 'awaiting_payment', 'slip_processing', 'under_review')
ORDER BY created_at DESC;ให้ตารางนี้อัปเดตจาก webhook ผ่าน version guard เสมอ
UPDATE deposits
SET status = $2,
is_terminal = $3,
version = $4,
late_slip_accepted_until = $5,
updated_at = now()
WHERE deposit_id = $1
AND version < $4;รายการค้างที่ต้องไม่หายไปจาก UI
สองสถานะนี้ยังต้องมีปุ่มอัปสลิป
expired และ canceled ที่ is_terminal: false ไม่ได้กินโควตาแล้ว แต่ยังต้องอยู่ในหน้า "รายการของฉัน" จนถึง late_slip_accepted_until เพราะผู้ใช้ที่โอนไปแล้วต้องกดอัปสลิปย้อนหลังได้
ถ้าหน้าจอคุณกรอง is_terminal = false อย่างเดียวก็จะรวมสองสถานะนี้อยู่แล้ว — แต่ถ้ากรองด้วย status IN (...) ต้องใส่ทั้งสองค่าเข้าไปด้วย ไม่งั้นผู้ใช้จะหาปุ่มอัปสลิปไม่เจอและจะโอนซ้ำ
ลำดับที่แนะนำ ตอนผู้ใช้กดเติมเงิน
- อ่านรายการค้างจากตารางของคุณ — มีค้างอยู่ไหม
- มี → ถามก่อนว่าจะ "โอนต่อ" หรือ "ยกเลิกแล้วสร้างใหม่"
- ไม่มี → เรียก
GET /v1/pay/sendersให้เลือกบัญชีผู้โอน POST /v1/pay/deposits- ถ้ายังได้
409อยู่ (เช่นผู้ใช้เปิดสองแท็บพร้อมกัน) → อ่านdetails.open_deposit_ids[]แล้วกลับไปข้อ 2
ขั้นต่อไป
- ยกเลิกรายการฝาก — วิธีปิดรายการค้างให้ถูกต้อง และตอนไหนที่ยกเลิกไม่ได้แล้ว
- สร้างรายการฝาก — error ทุกแบบพร้อม
detailsที่ต้องอ่าน - สถานะรายการฝาก — ความหมายของแต่ละสถานะ และการ poll ที่ไม่ชน rate limit
- ลำดับ webhook ไม่การันตี — version guard ที่ทำให้ตารางฝั่งคุณไม่ถอยหลัง
