ค้นหารายการ
หน้านี้คือเครื่องมือที่ทีมของคุณใช้ตอนลูกค้าทัก ไม่ใช่ทางที่โค้ดใช้ขับ business logic
การตัดสินใจเรื่องเงินใช้ webhook + version guard เท่านั้น
GET เป็น fallback ของ webhook ไม่ใช่ตัวหลัก · ถ้าจะ poll ให้ backoff 3 → 5 → 10 → 30 วินาที แล้วหยุดเมื่อรายการเป็น terminal poll ทุก 1 วินาทีทุกออร์เดอร์จะชน 429 แล้ว backoff ทั้ง API key ⇒ ออร์เดอร์อื่นสร้างไม่ได้ตามไปด้วย
ดูรายการฝากรายใบ
curl -sS "https://api.example-wl.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755749553" \
-H "PG-Nonce: 01JQA5T8V0X2Z4B6D8F0H2J4L6" \
-H "PG-Version: 2026-08-01" \
-H "PG-Signature: v1=8f2c1ab6e0d74f9b3c5a7e1d0f6b8a4c2e9d7b5a3c1f0e8d6b4a2c0e8f6d4b2a"{
"id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"object": "deposit_order",
"version": 6,
"status": "under_review",
"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",
"liquidity_source": "internal",
"review": {
"opened_at": "2026-08-21T04:19:08.601Z",
"escalated": false,
"four_eyes_required": false,
"estimated_resolution_bucket": "under_1h",
"reasons": [
{
"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
},
"sender": {
"bank_code": "002",
"account_number_masked": "xxx-x-x5567",
"account_name_th_masked": "ป*** ส***"
},
"metadata": { "user_id": "u_88213", "order_id": "ORD-9981" }
}verification{} — 6 มิติที่ตัดสินว่าผ่านอัตโนมัติหรือไม่
เป็น boolean ล้วน 6 ตัว ไม่มีค่าที่อ่านได้จากสลิปจริง
| ฟิลด์ | ตรวจอะไร |
|---|---|
amount_exact | ยอดตรงเป๊ะ ไม่มี tolerance |
within_time_window | เวลาบนสลิปอยู่ในหน้าต่างของรายการ |
sender_bank_match | ธนาคารผู้โอนตรงกับที่แจ้ง |
sender_account_match | เลขบัญชีผู้โอนตรงกับที่แจ้ง |
sender_name_match | ชื่อผู้โอนตรงกับที่แจ้ง |
receiver_match | บัญชีปลายทางตรงกับที่ระบบกำหนดให้ |
ยอดตรงต้องผ่านทั้ง 6 ตัว. ถ้า non-amount อีก 5 ตัวผ่านแต่ยอดไม่ตรง ระบบจะปิดอัตโนมัติทั้งสองทิศ: short-pay settled ที่ A=V<X, partial:true, settled_via:'auto'; same-slip overpay settled ตามยอดที่สร้างพร้อม overpayment{}. Amount mismatch อย่างเดียวไม่เปิด review
ทำไมเราไม่บอกว่า "ชื่อจริงบนสลิปคืออะไร"
เพราะผู้โอนจริงอาจเป็นคนละคนกับที่ merchant แจ้ง (สามีโอนแทนภรรยา) — ค่านั้นเป็นข้อมูลส่วนบุคคลของบุคคลที่สาม เราส่งได้แค่ matched: false และ ชื่อที่คุณเป็นคนส่งมาเอง แบบ mask กลับไปให้ (registered_name_masked) ผลคือ: ทีมคุณไล่เคสจากสลิปที่ลูกค้าส่งให้คุณโดยตรง ไม่ใช่จาก payload ของเรา
RECEIVER_MISMATCH เป็นเหตุผลเดียวที่ ไม่มี detail เลย — การบอกว่า "ควรเป็นบัญชี xxx1234" คือการยืนยันบัญชีของอีกฝ่ายให้คนที่โอนผิดรู้
ดูสลิป
GET /v1/pay/deposits/{id}/slipคืนไฟล์ภาพตรง ๆ (ต้องมี auth และรายการต้องอยู่ในขอบเขตบัญชีคุณ)
ไม่มี presigned URL ให้
เราไม่ส่ง URL ของที่เก็บไฟล์ออกไปเลย เพราะ URL ที่หลุดแปลว่าใครก็โหลดสลิปของลูกค้าได้ ถ้าหน้าเว็บของคุณต้องแสดงภาพ ให้ proxy ผ่าน backend ของคุณเอง อย่าฝัง URL ตรงในหน้า
ดูรายการถอนรายใบ
curl -sS "https://api.example-wl.com/v1/pay/withdrawals/wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755749553" \
-H "PG-Nonce: 01JQA5T8V0X2Z4B6D8F0H2J4L6" \
-H "PG-Version: 2026-08-01" \
-H "PG-Signature: v1=8f2c1ab6e0d74f9b3c5a7e1d0f6b8a4c2e9d7b5a3c1f0e8d6b4a2c0e8f6d4b2a"{
"id": "wdr_01K3PB2M7Q4RW8YT0NBVXH5DKC",
"object": "withdrawal_order",
"version": 7,
"status": "partially_filled",
"amount_total_minor": 1000000,
"fee_total_minor": 3000,
"filled_minor": 400000,
"remaining_minor": 600000,
"refunded_minor": 0,
"fee_recognized_minor": 1200,
"settled_splits": 1,
"max_splits": 4,
"currency": "THB",
"expires_at": "2026-08-21T16:12:00.000Z",
"fills": [
{
"leg_ref": "leg_7QF2H8KM",
"amount_minor": 400000,
"fee_recognized_minor": 1200,
"settled_at": "2026-08-21T05:02:11.045Z",
"receipt_state": "awaiting_confirm"
}
]
}remaining_minor ไม่ได้เท่ากับ amount_total_minor − filled_minor เสมอ
สูตรจริงคือ remaining = amount_total − filled − reservedreserved คือส่วนที่ มีคนกำลังโอนให้อยู่ แต่ยังไม่ settle ⇒ ใบที่ filled_minor เป็น 400000 และมีคนกำลังโอนอีก 200000 จะได้ remaining_minor เท่ากับ 400000 ไม่ใช่ 600000
ค้นหาแบบเป็นชุด
GET /v1/pay/deposits?status=under_review&updated_after=2026-08-21T00:00:00.000Z&cursor=&limit=200
GET /v1/pay/withdrawals?updated_after=2026-08-21T00:00:00.000Z&cursor=&limit=200ใช้แทนการ poll รายตัว · ดูกฎ cursor ที่ รายงานและการกระทบยอด
ให้ลูกค้าอ้างอิงด้วย deposit_id เสมอ
หน้าจอของผู้ใช้ปลายทางต้องแสดง deposit_id / withdrawal_id ให้กดคัดลอกได้ merchant_reference ของคุณเราค้นให้ไม่ได้ข้ามระบบ และรายการที่วิ่งผ่านช่องทางสำรองจะไม่มีเลขอ้างอิงอื่นให้เลย
ต่อเวลา
รายการฝากที่ยัง awaiting_payment แต่ลูกค้ากำลังจะโอนไม่ทัน ต่อเวลาได้จากระบบหลังบ้าน
curl -sS -X POST "https://api.example-wl.com/v1/bo/transactions/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/add-timeout" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Idempotency-Key: bo-timeout-dep01K3M7Y9-01"ทั้ง employee OIDC (transactions:write), step-up token ที่ bind กับ request นี้ และ Idempotency-Key เป็นค่าบังคับ · actor มาจาก OIDC sub ห้ามส่ง employee id เอง และ endpoint นี้ไม่รับ merchant API key/HMAC
สิ่งที่ต้องรู้ก่อนกด
| ข้อ | รายละเอียด |
|---|---|
ต่อได้เฉพาะตอน awaiting_payment | มีสลิปเข้ามาแล้วต่อไม่ได้ — รายการเดินไปเส้นตรวจสอบแล้ว |
| ค่าใหม่ถูก clamp ด้วยนโยบายของชั้นบน | ขอ 60 นาทีอาจได้ 20 นาที ⇒ 🔴 อ่าน expires_at ที่คืนกลับมา อย่าคิดเอง |
🔴 รายการที่ liquidity_source เป็น external_gateway ต่อเวลาไม่ได้ | expires_at ของรายการเหล่านี้มาจากช่องทางที่ใช้จริง (สั้นได้ถึง ~4 นาที) เราไม่มีสิทธิ์ยืดให้ |
| หน้าต่างรับสลิปย้อนหลังขยับตาม | นับจาก expires_at ใหม่ ⇒ ต่อเวลา = ขยับ late_slip_accepted_until ตามไปด้วย |
อย่า hardcode นับถอยหลังในหน้าเว็บ
เอา expires_at จาก response มาใช้เสมอ และ re-sync ทุกครั้งที่ผู้ใช้กลับเข้าหน้า (สลับแอปธนาคารไปแล้วกลับมา) countdown ที่ hardcode 15 นาทีจะเดินผิดทันทีที่รายการวิ่งผ่านช่องทางที่ TTL สั้นกว่านั้น
แก้รายการที่ติด under_review
POST /v1/bo/transactions/{id}/update-matching ถูกถอดออกและไม่ใช่ callable route เพราะไม่มี request contract ที่บอกได้ชัดว่าจะแก้อะไร ใช้ endpoint ที่มีสัญญาครบตามเหตุการณ์จริง 3 ทางด้านล่างแทน การตัดสินอนุมัติ/ปฏิเสธยังเกิดใน review decision endpoint ของเจ้าหน้าที่เท่านั้น
1. แนบหลักฐานเพิ่ม (ไม่รีเซ็ตสถานะ)
curl -sS -X POST "https://api.example-wl.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/evidence" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755749900" \
-H "PG-Nonce: 01JQA5TH9C7E1G3J5L7N9Q1S3V" \
-H "PG-Version: 2026-08-01" \
-H "Idempotency-Key: evid-dep01K3M7Y9-01" \
-H "PG-Signature: v1=1a3c5e70b9d2f486a0c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7" \
-H "Content-Type: application/json" \
-d '{
"kind": "bank_statement_line",
"reference": "merchant-ledger-2026-08-21#line-004921",
"note": "รายการเดินบัญชีที่ merchant เก็บไว้ยืนยันยอดโอน 5,000 บาท"
}'endpoint นี้รับเฉพาะ JSON reference ไม่รับไฟล์หรือ multipart · kind เป็น enum ปิด: bank_statement_line · chat_transcript · id_document_ref · other (id_document_ref คือเลขอ้างอิงในระบบ merchant ไม่ใช่รูปบัตร) การแนบเข้าเรื่องเดิม ไม่รีเซ็ต state และไม่ทำให้คิวเริ่มนับใหม่
2. แจ้งยอดที่โอนจริงในเคสที่ติด review เพราะเหตุอื่น (compatibility)
clean short-pay ที่ non-amount 5/5 ผ่านสำเร็จอัตโนมัติอยู่แล้ว จึงไม่ต้องเรียก endpoint นี้. ใช้ได้เฉพาะรายการที่ยังเป็น under_review เพราะมีเหตุ non-amount/ambiguity/policy อื่นจริง และยอดธนาคารยืนยันเป็น 0<V<X เท่านั้น
curl -sS -X POST "https://api.example-wl.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/force-complete" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755749950" \
-H "PG-Nonce: 01JQA5TK1E9G3J5L7N9Q1S3V5X" \
-H "PG-Version: 2026-08-01" \
-H "Idempotency-Key: forcecomplete-dep01K3M7Y9" \
-H "PG-Signature: v1=9f7d5b3a1c0e8f6d4b2a0c8e6f4d2b0a8c6e4f2d0b8a6c4e2f0d8b6a4c2e0f8d" \
-H "Content-Type: application/json" \
-d '{
"verified_amount_minor": 480000,
"note": "รายการติดตรวจชื่อผู้โอน ลูกค้าโอน 4,800 บาทแทน 5,000 บาท แนบหลักฐานเพื่อรับยอดจริงหลังตรวจชื่อ"
}'{
"id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B",
"object": "deposit_order",
"version": 7,
"status": "under_review",
"is_terminal": false
}202 ไม่ใช่การอนุมัติ
ชื่อ force-complete ทำให้เข้าใจผิดได้ง่าย — มันคือการ ขอให้เจ้าหน้าที่ตัดสิน ไม่ใช่การสั่งให้ปิดรายการ สถานะที่คืนกลับมาคือ under_review และเงินยังไม่ถูกเครดิต เงื่อนไข: รายการต้องติด review เพราะเหตุอื่นอยู่ก่อน · ต้องมีหลักฐาน authoritative ที่ derive 0<V<X · note อย่างน้อย 20 ตัวอักษร. ถ้า clean short-pay ปิดอัตโนมัติแล้ว endpoint จะตอบ 409 deposit_not_reviewable
หลังเจ้าหน้าที่ resolve เหตุอื่นและอนุมัติ reviewed short-pay คุณจะได้ deposit.settled ที่มี partial:true, settled_via:'review' และ ordered_amount_minor — เคสนี้ amount_received_minor (A) เท่ากับยอดธนาคารยืนยัน V และต่ำกว่า X; เครดิตผู้ใช้ด้วย A ไม่ใช่ยอดที่สั่งไว้. ส่วน clean short-pay จะได้ payload เดียวกันแต่ settled_via:'auto' โดยไม่ผ่านขั้นนี้
3. เจ้าหน้าที่ตัดสิน External route conflict
เส้นนี้ไม่ใช่ generic review approval และไม่ทำให้รายการฝาก settle. ใช้ได้เฉพาะ review ของ deposit ที่มีเหตุผลเดียว provider_conflict, มี authoritative External route, แถวล่าสุดมี can_decide=true และเจ้าหน้าที่ White Label/Agent คนปัจจุบันถือ live lease อยู่. Merchant ตัดสินรายการของตัวเองไม่ได้; slip, short-pay, overpayment, mixed reason และ Owner Tier-4 ยังใช้ workflow ของตนเอง.
Brand UI ขอ request-bound OIDC step-up จาก user gesture แล้วส่งผ่าน cookie session แบบ same-origin เท่านั้น — browser ห้ามถือหรือส่ง OIDC bearer token:
const reviewId = '40000000-0000-4000-8000-000000000001'
const body = {
decision: 'approve',
reason_code: 'EXTERNAL_RESULT_ACCEPTED',
decision_note: 'ตรวจผล External route และหลักฐานที่เกี่ยวข้องครบแล้ว ยืนยันผลรายการนี้',
}
await fetch(`/v1/bo/reviews/${reviewId}/decisions`, {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
'X-PG-BO-Client': '1',
'X-PG-Step-Up-Token': requestBoundAssertion,
'If-Match': '"3"',
'Idempotency-Key': 'review-decision-4000-approve-01',
},
body: JSON.stringify(body),
})Reject ใช้ URL เดิมและเปลี่ยน decision เป็น reject; proof ถูก bind เป็น review.approve/review.reject จึงสลับผลหลัง step-up ไม่ได้. สำเร็จจะปิด review task เป็น resolved, ล้าง lease และคืน ETag + Idempotent-Replay; endpoint ไม่แก้ deposit, route, evidence, match, reservation, journal, ledger, fee หรือ reserve. Legacy /approve และ /reject ถูก supersede และไม่มี runtime route.
สิ่งที่แก้ไม่ได้ — และทำอะไรแทน
| อยากแก้ | แก้ได้ไหม | ทำแทนอย่างไร |
|---|---|---|
| บัญชีผู้โอนของรายการฝาก | ❌ | ยังไม่โอน → ยกเลิกแล้วสร้างใหม่ · โอนไปแล้ว → อัปสลิปตามปกติ แล้วให้เข้าคิวตรวจสอบ |
| ยอดของรายการฝาก | ❌ | ยังไม่โอน → ยกเลิกแล้วสร้างใหม่ · โอนไปแล้วผิดเฉพาะยอด → รอผล deposit.settled อัตโนมัติ · ใช้ force-complete เฉพาะเมื่อรายการติด review เพราะเหตุอื่น |
ยอด/บัญชีปลายทางของรายการถอนที่ pending_approval | ❌ | ยกเลิกแล้วสร้างใหม่ (เงินถูกล็อกด้วยยอดเดิมไปแล้ว) |
| บัญชีปลายทางของรายการฝาก | ❌ | ระบบเป็นคนกำหนด ไม่ใช่คุณ และเปลี่ยนไม่ได้ |
merchant_reference | ❌ | unique ต่อบัญชีคุณ ตั้งครั้งเดียวตอนสร้าง |
รายการที่มีสลิปแล้ว ยกเลิกไม่ได้
POST /v1/pay/deposits/{id}/cancel จะได้ 409 deposit_not_cancelable เหตุผลตรงไปตรงมา: มีคนโอนเงินจริงไปแล้ว การยกเลิกรายการไม่ได้ทำให้เงินกลับมา เส้นทางที่ถูกคือปล่อยให้เดินไป settled หรือ rejected แล้วถ้าเงินต้องคืน ให้ไปที่ เรื่องร้องเรียน (claims)
ปลดบล็อกผู้ใช้ที่สร้างรายการไม่ได้
ผู้ใช้ที่มีรายการค้างเกินเพดานจะได้ 409 too_many_open_deposits พร้อม details.open_deposit_ids[]
curl -sS -X POST "https://api.example-wl.com/v1/bo/users/u_88213/clear-block" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Idempotency-Key: bo-clear-block-u88213-01" \
-H "Content-Type: application/json" \
-d '{
"reason_code": "OPEN_ORDERS_REVIEWED",
"note": "ตรวจรายการค้างและปิดรายการที่ไม่ใช้แล้วครบถ้วน"
}'คำสั่งนี้บังคับ employee OIDC (users:write) + step-up ที่ bind กับ request + Idempotency-Key; actor มาจาก OIDC sub และ immutable audit log ไม่รับ merchant API key/HMAC
ก่อนกดปลดบล็อก ให้ปิดรายการค้างก่อน
เพดานมีไว้กันผู้ใช้สร้างรายการยอดเดียวกันหลายใบ แล้วโอนครั้งเดียว — ซึ่งจะกลายเป็นรายการกำกวมที่จับคู่ไม่ได้ทั้งกอง วิธีที่ถูกคือ ยกเลิกรายการเก่าที่ผู้ใช้ไม่ได้จะโอนแล้ว ตาม open_deposit_ids[] ไม่ใช่ปลดเพดานทิ้ง
แผนที่สถานะสำหรับทีม support
status | เงินอยู่ไหน | บอกลูกค้าว่าอย่างไร |
|---|---|---|
awaiting_liquidity | ยังไม่โอน | กำลังจัดหาช่องทาง ยังไม่ต้องโอน 🔴 ไม่มี QR |
awaiting_payment | ยังไม่โอน | โอนได้เลย โอนจากบัญชีที่แจ้งไว้เท่านั้น |
slip_processing | โอนแล้ว รอตรวจ | กำลังตรวจสลิป 🔴 ห้ามปล่อยของ |
under_review | โอนแล้ว รอตรวจ | ใช้ข้อความจาก review.reasons[0].display_th ตรง ๆ + "เงินไม่หาย ไม่ต้องโอนซ้ำ" |
settled | เครดิตแล้ว | สำเร็จ |
settled + overpayment{} | เครดิตตามยอดที่สร้าง; ส่วนเกินอยู่ใน claim | รายการสำเร็จแล้ว · แจ้งยอดเกิน/claim id และให้ลูกค้าติดต่อ support · ห้ามบอกว่าคืนแล้ว |
expired | ปกติยังไม่โอน | ถ้า is_terminal เป็น false → ยังมีปุ่ม "ฉันโอนไปแล้วแต่หมดเวลา" |
canceled | ปกติยังไม่โอน | เหมือน expired ถ้ามี late_slip_accepted_until |
rejected | ไม่เครดิต | ใช้ rejection.display_th + แสดง deposit_id |
reversed | หักคืนแล้ว | ต้องมี handler ตั้งแต่วันแรก event นี้ปิดไม่ได้ |
เช็ค is_terminal ไม่ใช่ชื่อสถานะ
expired และ canceled ไม่ใช่จุดจบ ถ้ายังมี late_slip_accepted_until ทีมที่ปิดเคสทันทีที่เห็น canceled จะเจอ deposit.settled ของรายการที่ปิดไปแล้ว 20 นาทีต่อมา
ขั้นต่อไป
- เรื่องร้องเรียน (claims) — ลูกค้าโอนแล้วแต่ไม่มีรายการรองรับ
- สถานะและ state ทั้ง 9 ค่า — ความหมายเต็มของทุกสถานะ
- เข้าคิวตรวจสอบ (under_review) และทางเลือกที่มี
- บันทึกและ audit log — ไล่ดู request/response ย้อนหลังของรายการนี้
- รายงานและการกระทบยอด — ค้นแบบเป็นชุดด้วย cursor
