บัญชีผู้โอน
หน้านี้คือหน้าที่ตัดสินว่าการต่อของคุณจะราบรื่นหรือจะมีคิวเจ้าหน้าที่ล้น · ถ้าอ่านได้หน้าเดียวในเว็บนี้ ให้อ่านหน้านี้
กฎเดียวที่ต้องจำ
senderคือ "บัญชีที่นิ้วจะกดโอนจริง" ไม่ใช่ "เจ้าของบัญชีผู้ใช้ในระบบของคุณ"
ถ้าสามีเป็นคนกดโอนให้ภรรยา sender คือบัญชีของสามี ถ้าโอนจากบัญชีบริษัท sender คือบัญชีบริษัท — ตัวตนที่จะไปปรากฏบนสลิปคือตัวตนที่ต้องส่งมา
ทำไมต้องแจ้งบัญชีผู้โอน
ระบบนี้ไม่มีบัญชีกลางในเส้นทางปกติ เงินที่ผู้ใช้โอนไม่ได้เข้าบัญชีของ gateway แต่เข้า บัญชีส่วนตัวของผู้ถอนอีกคน โดยตรง
ผลที่ตามมาคือ บัญชีปลายทางเดียวกันอาจมีเงินเข้าจากหลายคนในเวลาไล่เลี่ยกัน ระบบต้องรู้ให้ได้ว่าสลิปใบที่เพิ่งอัปเข้ามาเป็นของรายการไหน
| วิธีแก้ | ใครใช้ | ปัญหา |
|---|---|---|
| ใส่เศษสตางค์ให้ต่างกัน (1,000.37 / 1,000.42) | gateway ทั่วไปในตลาด | ผู้ใช้พิมพ์ยอดผิดตลอด · พอ volume สูงเศษสตางค์ก็ชนกันเอง · ยอดไม่ตรงราคาสินค้า |
| ใช้ตัวตนของผู้โอน | ระบบนี้ | ยอดเป็นเลขกลมเสมอ ตรงกับราคาสินค้าเป๊ะ · แลกมาด้วยการที่ merchant ต้องถามบัญชีผู้โอนก่อนสร้างรายการ |
ไม่ส่งบัญชีผู้โอน = สร้างรายการไม่ได้
POST /v1/pay/deposits ที่ไม่มีทั้ง sender และ sender_id จะได้ 422 sender_required และ ไม่มีรายการถูกสร้าง · นี่ไม่ใช่ field ที่จะ "เติมทีหลัง" ได้
ระบบตรวจอะไรบ้าง — 6 มิติ
สลิปที่อัปเข้ามาถูกตรวจ 6 มิติ: ยอดตรงต้องผ่านทั้ง 6 จึงเป็น exact · เมื่อ non-amount อีก 5 มิติผ่าน clean short-pay จะ settle อัตโนมัติที่ A=V<X และ same-slip overpay จะ settle ตามยอด order พร้อม claim · มีเหตุ non-amount/ambiguity/policy อื่นจริงจึงเข้าคิวเจ้าหน้าที่
| มิติ | field ใน verification{} | ถ้าไม่ผ่านจะได้ reasons[].code |
|---|---|---|
| ยอดตรงเป๊ะ (ไม่มี tolerance) | amount_exact | amount-only short-pay → settleยอดจริงอัตโนมัติ; overpay ที่อีก 5 มิติผ่าน → settleยอด orderพร้อม claim/flag; AMOUNT_MISMATCH จะอยู่ใน review ได้เมื่อมีเหตุอื่นร่วมด้วยเท่านั้น |
| เวลาโอนอยู่ในช่วงของรายการ | within_time_window | TIME_WINDOW |
| ธนาคารต้นทางตรง | sender_bank_match | SENDER_BANK_MISMATCH |
| เลขบัญชีต้นทางตรง | sender_account_match | SENDER_ACCOUNT_MISMATCH |
| ชื่อบัญชีต้นทางตรง | sender_name_match | SENDER_NAME_MISMATCH |
| บัญชีปลายทางตรง | receiver_match | RECEIVER_MISMATCH |
verification{} ที่ส่งกลับมาเป็น boolean ล้วน 6 ตัว ไม่มีค่าที่อ่านได้จากสลิปจริง เพราะผู้โอนจริงอาจไม่ใช่คนที่คุณแจ้งมา (โอนจากบัญชีภรรยา) — ค่านั้นเป็นข้อมูลส่วนบุคคลของบุคคลที่สาม
3 ใน 6 มิติเป็นเรื่องบัญชีผู้โอนล้วน ๆ ⇒ คุณภาพของข้อมูลที่คุณส่งมาตอนสร้างรายการ คือสิ่งที่กำหนดอัตราการเข้าคิวโดยตรง
ผู้ใช้โอนจากบัญชีอื่นแล้วเกิดอะไรขึ้น
โอนจากบัญชีอื่น
→ สลิปยังถูกรับตามปกติ (ไม่มีการปฏิเสธเงินที่โอนมาแล้ว)
→ status = under_review, reasons = ['SENDER_ACCOUNT_MISMATCH']
→ เจ้าหน้าที่ตรวจสอบ
→ settled (ปกติจบแบบนี้) หรือ rejected (ถ้าตรวจแล้วไม่ใช่เงินของรายการนี้จริง)สิ่งที่ระบบจะไม่ทำเด็ดขาด
- ❌ ไม่ auto-reject เพราะบัญชีไม่ตรง
- ❌ ไม่ auto-approve เมื่อรายการรอนาน (จะกลายเป็น
review.escalated: trueแทน) - ❌ ไม่ปฏิเสธสลิปของธนาคารที่เพิ่งถูกปิดรับ (
BANK_DISABLED→ เข้าคิวเจ้าหน้าที่)
สิ่งที่ merchant ต้องไม่ทำ
สามข้อนี้ทำแล้วเสียเงินจริง
- ห้ามขึ้นข้อความ "โอนผิดบัญชี เงินจะไม่เข้า" — ไม่จริง เงินเข้าแน่นอนถ้าเจ้าหน้าที่ตรวจแล้วเป็นเงินของรายการนี้
- ห้าม auto-refund หรือ auto-cancel เมื่อเห็น
under_review— รายการยังเดินอยู่ ยกเลิกไม่ได้แล้วด้วย - ห้ามมีปุ่ม "โอนใหม่อีกครั้ง" บนหน้า
under_review— ผู้ใช้จะโอนซ้ำ กลายเป็นเงินเข้าสองรอบที่ต้องตามคืน
UI 3 หน้าที่ต้องมี
หน้า 1 — ก่อนสร้างรายการ (แก้ปัญหาได้ ~90%)
ให้ผู้ใช้ เลือก ไม่ใช่ให้ พิมพ์
คุณจะโอนจากบัญชีไหน
( ) กสิกรไทย xxx-x-x1234 สมชาย ใจดี
( ) ไทยพาณิชย์ xxx-x-x9876 สมชาย ใจดี
( ) + ใช้บัญชีอื่น
หมายเหตุ: ระบบจะจับคู่สลิปกับบัญชีนี้
ถ้าโอนจากบัญชีอื่น รายการจะต้องรอเจ้าหน้าที่ตรวจสอบรายการในลิสต์มาจาก GET /v1/pay/senders?end_user_ref= แล้วส่ง sender_id กลับมาตอนสร้างรายการ
หน้า 2 — หน้า QR (กล่องนี้ต้องเด่นกว่าตัว QR)
┌──────────────────────────────────────────────┐
│ โอนจากบัญชีนี้เท่านั้น │
│ │
│ กสิกรไทย xxx-x-x1234 │
│ สมชาย ใจดี │
│ │
│ ถ้าจะใช้บัญชีอื่น กด "เปลี่ยนบัญชีผู้โอน" │
│ ก่อนโอน (โอนไปแล้วเปลี่ยนไม่ได้) │
│ │
│ [ เปลี่ยนบัญชีผู้โอน ] │
│ │
│ QR นี้ใช้ได้ครั้งเดียวสำหรับรายการนี้ │
│ ห้ามบันทึกภาพไว้ใช้ครั้งหน้า │
└──────────────────────────────────────────────┘ปุ่ม "เปลี่ยนบัญชีผู้โอน" ทำงานแบบนี้
POST /v1/pay/deposits/{id}/cancel— ยกเลิกได้เฉพาะตอนที่ยังไม่มีสลิป- สร้างรายการใหม่ด้วย
senderใหม่ และIdempotency-Keyใหม่ (body เปลี่ยน = key ต้องเปลี่ยน)
การให้ทางออกก่อนโอน ถูกกว่าการปล่อยให้เข้าคิวเจ้าหน้าที่หลายเท่า
หน้า 3 — หลังโอนแล้วบัญชีไม่ตรง
เราได้รับสลิปของคุณแล้ว
บัญชีที่โอนไม่ตรงกับที่แจ้งไว้ เจ้าหน้าที่กำลังตรวจสอบ
เงินของคุณไม่หาย และไม่ต้องโอนซ้ำใช้ข้อความที่เราส่งมาให้ ไม่ต้องเขียนเอง
ข้อความจริงให้ดึงจาก review.reasons[0].display_th ใน webhook deposit.under_review — เราเขียนมาให้ครบ 3 ประโยคเสมอ: (1) เราได้รับสลิปแล้ว (2) ติดเพราะอะไร (3) เงินไม่หาย ไม่ต้องโอนซ้ำ
ถ้าจะบอกเวลา ให้ใช้ review.estimated_resolution_bucket (under_15m · under_1h · under_4h · extended) เท่านั้น 🔴 ห้าม hard-code "ไม่เกิน 15 นาที" เพราะเวลาตรวจเป็นของผู้ให้บริการแต่ละราย
สีของกล่องต้องเป็นเหลืองหรือฟ้า ไม่ใช่แดง — นี่คือสถานะรอตรวจ ไม่ใช่ error
ข้อความไทยที่เอาไปใช้ได้เลย
คัดลอกไปใช้ได้ทันที ปรับชื่อแบรนด์ตามของคุณ
| จุดในหน้าจอ | ข้อความ |
|---|---|
| ใต้ตัวเลือกบัญชีผู้โอน | ระบบจะจับคู่สลิปกับบัญชีนี้ ถ้าโอนจากบัญชีอื่น รายการจะต้องรอเจ้าหน้าที่ตรวจสอบ |
| หัวกล่องบนหน้า QR | โอนจากบัญชีนี้เท่านั้น |
| ใต้กล่องบนหน้า QR | ถ้าจะใช้บัญชีอื่น กดเปลี่ยนบัญชีผู้โอนก่อนโอน โอนไปแล้วเปลี่ยนไม่ได้ |
| ใต้ QR | QR นี้ใช้ได้ครั้งเดียวสำหรับรายการนี้ ห้ามบันทึกภาพไว้ใช้ครั้งหน้า |
| ปุ่มยืนยันก่อนโอน | ฉันจะโอนจากบัญชี กสิกรไทย xxx-x-x1234 สมชาย ใจดี |
หลังอัปสลิป (slip_processing) | กำลังตรวจสอบสลิปกับธนาคาร ใช้เวลาไม่เกิน 1 นาที |
บัญชีไม่ตรง (under_review) | เราได้รับสลิปของคุณแล้ว บัญชีที่โอนไม่ตรงกับที่แจ้งไว้ เจ้าหน้าที่กำลังตรวจสอบ เงินของคุณไม่หาย และไม่ต้องโอนซ้ำ |
ชื่อไม่ตรง (under_review) | เราได้รับสลิปของคุณแล้ว ชื่อบัญชีผู้โอนไม่ตรงกับที่แจ้งไว้ เจ้าหน้าที่กำลังตรวจสอบ เงินของคุณไม่หาย และไม่ต้องโอนซ้ำ |
| หน้ารอเจ้าหน้าที่ (ปุ่มที่ต้องไม่มี) | ❌ โอนใหม่อีกครั้ง ❌ ยกเลิกรายการ ❌ ขอคืนเงิน |
| ฟอร์มบัญชีใหม่ ใต้ช่องชื่อ | กรอกชื่อตามที่ปรากฏบนสลิป ไม่ต้องใส่คำนำหน้า เช่น นาย นาง น.ส. |
| ธนาคารที่ปิดรับ | ธนาคารนี้ปิดรับรายการใหม่ชั่วคราว กรุณาเลือกธนาคารอื่น |
ลดการเข้าคิวด้วย GET /v1/pay/senders
endpoint นี้คืนบัญชีที่ผู้ใช้คนนี้เคยใช้โอนสำเร็จมาแล้ว ⇒ ผู้ใช้ไม่ต้องพิมพ์อะไรเลย
curl -G https://api.example-wl.com/v1/pay/senders \
--data-urlencode "end_user_ref=u_88213" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755749553" \
-H "PG-Nonce: 01JQA5T8V0X2Z4B6D8F0H2J4L7" \
-H "PG-Version: 2026-08-01" \
-H "PG-Signature: v1=4b18...c07a"{
"data": [
{
"sender_id": "snd_01K2QF7V8H3NPD0YXW4RJ6ZB5C",
"bank_code": "004",
"bank_name_th": "ธนาคารกสิกรไทย",
"account_number_masked": "xxx-x-x1234",
"account_name_th_masked": "ส*** ใ***",
"last_used_at": "2026-08-18T11:42:07.315Z",
"auto_approved_count": 7
},
{
"sender_id": "snd_01K2QF7V8H3NPD0YXW4RJ6ZB6D",
"bank_code": "014",
"bank_name_th": "ธนาคารไทยพาณิชย์",
"account_number_masked": "xxx-x-x9876",
"account_name_th_masked": "ส*** ใ***",
"last_used_at": "2026-07-30T08:15:52.004Z",
"auto_approved_count": 2
}
]
}endpoint นี้ลดการเก็บข้อมูลบัญชีที่ไม่จำเป็น
เราคืน sender_id ไม่คืนเลขบัญชีเต็ม ⇒ คุณไม่ต้องเก็บเลขบัญชีเต็มไว้ในฐานข้อมูลของคุณเลย เก็บแค่ sender_id กับค่า masked ไว้แสดงผลก็พอ
// Node — เรียงบัญชีที่ใช้ล่าสุดขึ้นก่อน แล้วให้ผู้ใช้เลือก
async function senderChoices(endUserRef) {
const path = '/v1/pay/senders'
const query = `end_user_ref=${encodeURIComponent(endUserRef)}`
const res = await fetch(`https://api.example-wl.com${path}?${query}`, {
headers: sign('GET', path, query, '', null),
})
const { data } = await res.json()
return data
.sort((a, b) => Date.parse(b.last_used_at) - Date.parse(a.last_used_at))
.map((s) => ({
value: s.sender_id, // ส่งค่านี้กลับตอนสร้างรายการ
label: `${s.bank_name_th} ${s.account_number_masked} ${s.account_name_th_masked}`,
familiar: s.auto_approved_count > 0, // เคยผ่านอัตโนมัติมาแล้ว
}))
}ผู้ใช้ใหม่ที่ต้องพิมพ์เอง — validate ฝั่ง client ก่อนส่ง
ทำ 4 ข้อนี้แล้วอัตราการเข้าคิวจะลดลงชัดเจน
| ทำอะไร | เหตุผล |
|---|---|
ตัด - และช่องว่างออกจาก account_number ให้เหลือตัวเลขล้วน | ธนาคารส่งเลขบัญชีมาแบบไม่มีตัวคั่น |
ตัดคำนำหน้า นาย นาง น.ส. นางสาว คุณ ออกจาก account_name_th | 🔴 ธนาคารไม่ส่งคำนำหน้ามาในสลิป ใส่ไปคือชื่อไม่ตรงทันที |
| ยุบช่องว่างซ้อนให้เหลือช่องเดียว และ trim หัวท้าย | ชื่อที่มีช่องว่างเกินจะไม่ตรง |
ถ้าผู้ใช้พิมพ์ชื่ออังกฤษลงช่องภาษาไทย ให้เตือนและย้ายไป account_name_en | สองช่องนี้ถูกเทียบคนละแบบ |
// Node — ทำความสะอาดก่อนส่งเข้า POST /v1/pay/deposits
const HONORIFICS = /^(นาย|นางสาว|น\.ส\.|นาง|คุณ|ด\.ช\.|ด\.ญ\.)\s*/
function cleanSender(input) {
const accountNumber = input.accountNumber.replace(/[^0-9]/g, '')
const nameTh = input.nameTh.replace(HONORIFICS, '').replace(/\s+/g, ' ').trim()
if (accountNumber.length < 8) throw new Error('เลขบัญชีสั้นเกินไป')
if (/^[A-Za-z\s.]+$/.test(nameTh)) {
throw new Error('ช่องนี้ต้องเป็นชื่อภาษาไทย ถ้าสลิปแสดงชื่ออังกฤษให้กรอกในช่องชื่ออังกฤษ')
}
return {
bank_code: input.bankCode, // ต้องมาจาก GET /v1/pay/banks ที่ available = true
account_number: accountNumber,
account_name_th: nameTh,
...(input.nameEn ? { account_name_en: input.nameEn.toUpperCase().trim() } : {}),
}
}<?php
// PHP — เวอร์ชันเดียวกัน
function cleanSender(array $in): array
{
$account = preg_replace('/[^0-9]/', '', $in['account_number']);
$nameTh = preg_replace('/^(นาย|นางสาว|น\.ส\.|นาง|คุณ|ด\.ช\.|ด\.ญ\.)\s*/u', '', $in['account_name_th']);
$nameTh = trim(preg_replace('/\s+/u', ' ', $nameTh));
if (strlen($account) < 8) {
throw new InvalidArgumentException('เลขบัญชีสั้นเกินไป');
}
$out = [
'bank_code' => $in['bank_code'],
'account_number' => $account,
'account_name_th' => $nameTh,
];
if (!empty($in['account_name_en'])) {
$out['account_name_en'] = trim(mb_strtoupper($in['account_name_en']));
}
return $out;
}🔴 ธนาคารในฟอร์มต้องเทาตาม GET /v1/pay/banks ไม่งั้นผู้ใช้จะไปเจอ 409 sender_bank_unavailable ที่ขั้นสุดท้าย ซึ่งเป็นจุดที่คนทิ้งรายการมากที่สุด
เคสบุคคลที่สาม
| เคส | วิธีที่ถูก | ผลที่ได้ |
|---|---|---|
| สามีโอนให้ภรรยา | ส่ง sender = บัญชีของสามี (คนที่จะกดโอน) ไม่ใช่บัญชีของภรรยาที่เป็นเจ้าของ account | settled อัตโนมัติ |
| ลืมแจ้งแล้วให้คนอื่นโอน | ยังไม่โอน → cancel แล้วสร้างใหม่ · โอนไปแล้ว → อัปสลิปตามปกติ | เส้นหลังเข้า under_review แล้วเจ้าหน้าที่ approve |
| โอนจากบัญชีบริษัท | account_name_th = ชื่อนิติบุคคลตามที่ปรากฏบนสลิป เช่น บริษัท เอบีซี จำกัด ไม่ใช่ชื่อกรรมการ · ส่ง account_name_en มาด้วยถ้าสลิปแสดงชื่ออังกฤษ | ครั้งแรกอาจเข้า under_review เพราะแต่ละธนาคารตัดคำต่างกัน · ผ่านครั้งแรกแล้วบัญชีจะโผล่ใน /v1/pay/senders ครั้งต่อไปผ่านอัตโนมัติทุกครั้ง |
| ลูกค้า B2B ที่ใช้ประจำ | ให้เติมยอดเล็กครั้งแรกเพื่อ "ลงทะเบียนบัญชี" | เป็น onboarding pattern ที่เราแนะนำ |
นโยบายการรับเงินจากบุคคลที่สาม
ถ้านโยบายของคุณห้ามรับเงินจากบุคคลที่สาม ให้บังคับที่ UI ของคุณเอง เช่น แสดงเฉพาะบัญชีที่ชื่อตรงกับเจ้าของ account · อย่าคาดหวังให้ gateway ปฏิเสธให้ — เราจะไม่ปฏิเสธเงินที่โอนมาแล้ว เราจะส่งเข้าคิวเจ้าหน้าที่แทน
เทียบกับที่คุณเคยต่อมา
| gateway ที่คุณเคยต่อ | ที่นี่ | |
|---|---|---|
| ระบุรายการด้วย | เศษสตางค์ / reference ใน memo | ตัวตนของผู้โอน |
| ยอดที่ผู้ใช้เห็น | 1,000.37 | 1,000.00 |
| ต้องส่งบัญชีผู้โอนตอนสร้าง | ไม่ต้อง | ต้อง ไม่งั้น 422 sender_required |
| โอนจากบัญชีอื่น | มักตรวจไม่เจอ ต้องเปิด ticket เอง | เข้าคิวเจ้าหน้าที่อัตโนมัติ แล้วจบเป็น settled เป็นส่วนใหญ่ |
| หน้าจอที่ต้องเพิ่ม | — | หน้าเลือกบัญชีผู้โอน + กล่องเตือนบนหน้า QR |
สิ่งที่คุณต้องทำเพิ่มจริง ๆ มีแค่ 2 อย่าง: หน้าเลือกบัญชีผู้โอนก่อนสร้างรายการ และกล่อง "โอนจากบัญชีนี้เท่านั้น" บนหน้า QR · แลกกับการที่ยอดตรงราคาสินค้าเป๊ะและไม่มีปัญหาเศษสตางค์ชนกัน
ขั้นต่อไป
- สร้างรายการฝาก — ที่ที่
sender/sender_idถูกส่งเข้าไปจริง - ธนาคารที่เปิดรับ — เทาปุ่มธนาคารในฟอร์มบัญชีผู้โอน
- อัปโหลดสลิป — ขั้นถัดจากผู้ใช้กดโอน
- สถานะรายการฝาก —
under_reviewแปลว่าอะไร และห้ามทำอะไรตอนนั้น - ยกเลิกรายการฝาก — เบื้องหลังปุ่ม "เปลี่ยนบัญชีผู้โอน"
