อภิธานศัพท์
คำที่ใช้ทั้งเอกสาร พร้อมที่ที่คุณจะเจอมันจริง ๆ ในโค้ด
อ่านสามคู่นี้ก่อน ถ้ามีเวลาแค่นาทีเดียว
| คู่ | ต่างกันอย่างไร |
|---|---|
amount_received_minor ↔ credited_amount_minor | ตัวแรกเครดิตให้ ผู้ใช้ปลายทาง · ตัวหลังคือยอดที่เข้า กระเป๋า merchant ของคุณ |
leg ↔ fill | leg คือ ปลายทางที่ต้องโอนไป (ฝั่งฝาก) · fill คือ เงินที่ไหลเข้ามาแล้วก้อนหนึ่ง (ฝั่งถอน) |
pending_clearing ↔ available | เครดิตแล้วแต่ ยังถอนไม่ได้ ↔ ถอนได้จริง (ห่างกัน 72 ชั่วโมง) |
คนและรายการ
ผู้ฝาก (depositor) ผู้ใช้ปลายทางของคุณที่กำลังเติมเงิน · เป็นคนที่กดโอนจากแอปธนาคารจริง
ผู้ถอน (withdrawer) เจ้าของบัญชีธนาคารที่รับเงินโอน · เป็น ลูกค้าของ merchant อีกราย ไม่ใช่บัญชีของ gateway · นี่คือเหตุผลที่ระบบนี้ไม่มีบัญชีกลางในเส้นทางปกติ
รายการฝาก (deposit order) — dep_... ใบสั่งเติมเงินหนึ่งใบ · มี status 9 ค่า · id ห้าม parse ห้ามเดา prefix คอลัมน์ต้องเป็น text
รายการถอน (withdrawal order) — wdr_... ใบสั่งถอนหนึ่งใบ · มี status 7 ค่า · หนึ่งใบรับเงินจากผู้ฝากได้หลายคน
merchant_reference เลขอ้างอิงฝั่งคุณ ต้อง unique ต่อ merchant ยาวไม่เกิน 64 ตัวอักษร · ใช้ค้นย้อนกลับได้ แต่ deposit_id คือ primary key ที่ support ใช้ตามเรื่อง
end_user_ref id ของผู้ใช้ในระบบคุณ · ใช้ผูกกับ GET /v1/pay/senders และ GET /v1/pay/beneficiaries · 🔴 เป็นค่าที่คุณกรอกเอง ปลอมได้ ระบบจึงไม่เคยใช้ค่านี้เดี่ยว ๆ เป็นมิติของ rate limit
ฝั่งการโอน
leg — legs[]ปลายทางหนึ่งจุดที่ผู้ใช้ต้องโอนไป · ยอด 5,000 อาจถูกซอยเป็น 2 leg (3,000 + 2,000) ไปคนละบัญชี ⇒ UI ต้องรองรับ "ขั้นที่ 1 จาก 2" 🔴 legs[0] ไม่ใช่ทั้งหมด
leg_ref id ของ leg ที่คุณเห็น · เป็นค่าที่ผ่านการแปลงมาแล้ว ⇒ merchant สองรายที่อยู่คนละฝั่งของรายการเดียวกันจะได้ค่าคนละตัว join กันไม่ได้ · ใช้เป็น key ของบรรทัด timeline ฝั่งคุณ
leg_status — pending | complete สถานะภายในของ leg ที่สะท้อนออกมาให้เห็น · 🔴 complete ไม่ได้แปลว่าเงินเข้า ตัวที่แปลว่าเงินเข้าคือ status === 'settled' เท่านั้น · ใช้ทำ progress bar ได้ ห้ามใช้ตัดสินใจเรื่องเงิน
qr_payload สตริง PromptPay สำหรับ render QR · ข้างในมี PromptPay ID ของผู้ถอนโดยหลีกเลี่ยงไม่ได้ ⇒ 🔴 ห้าม log ห้ามเก็บลง DB ห้ามส่งต่อ ใช้แสดงผลแล้วทิ้ง · มีอายุเท่ากับ expires_at
expires_at เวลาหมดอายุจริงของรายการ · 🔴 อ่านจาก response ทุกครั้ง ห้าม hardcode — บาง route หมดอายุจริงใน ~4 นาที ไม่ใช่ 15–20 นาที
ฝั่งการรับเงิน
fillเงินหนึ่งก้อนที่ไหลเข้ารายการถอน · ถอน 10,000 ที่ได้ 4,000 + 6,000 = 2 fill ⇒ webhook withdrawal.filled สองครั้ง ไม่ใช่ครั้งเดียวตอนจบ · withdrawal.filled เป็น event เดียวในระบบที่ยิงหลายครั้งต่อ object เดียว
allocation การจับคู่ระหว่างรายการฝากหนึ่งใบกับรายการถอนหนึ่งใบที่ระบบทำอยู่ภายใน · 🔴 merchant ไม่เห็น object นี้เลย ทั้งชื่อ id และสถานะของมัน — สิ่งที่คุณเห็นคือเงาของมันสองรูปแบบ: legs[] ฝั่งฝาก และ fills[] / withdrawal.filled ฝั่งถอน ⇒ ไม่ต้องมีตารางรองรับ allocation ในฐานข้อมูลของคุณ
filled_minor / remaining_minor / reserved_minor ยอดที่ได้แล้ว / ยอดที่ยังขาด / ยอดที่มีคนกำลังโอนมาแต่ยังไม่ถึง 🔴 แหล่งความจริงคือค่าที่มากับ payload ผ่าน version guard ห้าม SUM() ตารางฝั่งคุณ ห้าม +=
settled_splits / max_splits จำนวนก้อนที่ปิดไปแล้ว / เพดานจำนวนก้อนของใบนั้น · ใช้ทำ UI ว่า "ได้แล้ว 2 จาก 4 ก้อน"
partial fill สภาพที่รายการถอนได้เงินมาบางส่วน · status เป็น partially_filled · 🔴 ปิดรายการได้เมื่อ status === 'settled' และ remaining_minor === 0 เท่านั้น
fee_recognized_minor ค่าธรรมเนียมส่วนที่รับรู้ไปแล้วตามสัดส่วนที่ปิดจริง · ค่าธรรมเนียมแบบคงที่ผูกกับ ใบ ไม่ใช่ต่อ fill ⇒ ซอย 4 ก้อนไม่ได้จ่ายคงที่ 4 เท่า 🔴 อย่าคำนวณค่าธรรมเนียมเองต่อ fill
เงินในกระเป๋า
balance_transaction — btxn_... บรรทัดการเคลื่อนไหวของกระเป๋าคุณ · immutable ⇒ ไม่มี version ไม่มี previous_attributes ⇒ dedupe ด้วย id และ สะสมยอด ห้ามเขียนทับด้วย balance_after
bucket — available | pending_clearing | reserved
| bucket | หมายถึง | ถอนออกได้ไหม |
|---|---|---|
pending_clearing | เพิ่งเครดิต ยังอยู่ในหน้าต่าง 72 ชั่วโมง | ❌ ได้ 402 |
available | ผ่าน clearing แล้ว | ✅ |
reserved | ถูกล็อกไว้กับรายการถอนที่เปิดอยู่ | ❌ |
clearing กระบวนการย้ายยอดจาก pending_clearing ไป available เมื่อครบ 72 ชั่วโมง นับจาก settled_at
- เวลาที่จะย้ายบอกไว้ล่วงหน้าที่
clearing_available_atในตัวรายการฝาก - ตอนย้ายจริงคุณจะได้ webhook
balance.cleared - 🔴
balance.clearedคือ การย้ายช่อง ไม่ใช่เงินเข้าใหม่ — บวกยอดซ้ำตอนนี้ = ยอดเป็นสองเท่า
ทำไมต้องมี 72 ชั่วโมง — ผู้ถอนโต้แย้งได้ภายในหน้าต่างนี้ ถ้าเรื่องนั้นจริงต้องกลับรายการ · การกลับรายการภายใน 72 ชั่วโมงตัดจาก pending_clearing ⇒ ยอด available ของคุณไม่ติดลบ
เมื่อเรื่องไม่เป็นไปตามแผน
under_review สลิปเข้ามาแล้วแต่มีมิติ non-amount ไม่ผ่าน / กำกวม / ติด policy ⇒ เข้าคิวเจ้าหน้าที่; short-pay ที่ผิดเฉพาะยอดและอีก 5 มิติผ่านจะ settle อัตโนมัติที่ยอดจริง ส่วน overpay จะ settle ตามยอด order พร้อม claim/flag 🔴 ไม่ใช่ error ไม่ใช่การปฏิเสธเงิน · แสดง review.reasons[0].display_th ตรง ๆ · สีเหลือง/ฟ้า ไม่ใช่แดง · ห้ามมีปุ่ม "โอนใหม่"
6 มิติ — verification{}amount_exact · within_time_window · sender_bank_match · sender_account_match · sender_name_match · receiver_match · ทั้งหมดเป็น boolean ล้วน · exact ต้องครบ 6; เมื่ออีก 5 ตัวผ่าน short-pay จะ settle อัตโนมัติที่ A=V<X และ overpay จะ settle ตามยอด order พร้อม claim; amount mismatch อย่างเดียวไม่เข้า review
review.estimated_resolution_bucket — under_15m | under_1h | under_4h | extended ช่วงเวลาคร่าว ๆ ที่เอาไปบอกผู้ใช้ได้ · 🔴 นี่เป็น ตัวเดียว ที่ใช้บอกเวลาได้ ห้าม hardcode "ไม่เกิน 15 นาที" เพราะกรอบเวลาเป็นของ White Label แต่ละราย
review.escalated เรื่องค้างนานเกินกรอบแล้วถูกยกระดับ · จะได้ deposit.under_review ซ้ำ พร้อม version ใหม่ 🔴 ไม่มีการอนุมัติอัตโนมัติเมื่อเวลาหมด และจะไม่มีวันมี — ไม่มี event ชื่อ deposit.auto_approved
late-slip — late_slip_accepted_until หน้าต่างรับสลิปย้อนหลัง 24 ชั่วโมง สำหรับคนที่โอนแล้วปิดเบราว์เซอร์ หรือกด cancel แล้วเผลอโอน ⇒ expired และ canceled ไม่ใช่จุดจบ ถ้ายังมีค่านี้ · หมดหน้าต่างแล้วอัปสลิปจะได้ 410 late_slip_window_closed + claim_url
is_terminal 🔴 ตัวเดียวที่ใช้ตัดสินว่าปิดรายการได้หรือยัง — ไม่ใช่ชื่อ status · expired และ canceled ที่ยังรับ late-slip ได้ จะมีค่านี้เป็น false · settled เองก็เป็น false เพราะยังออกไป reversed ได้
หน้าต่างข้อพิพาท (dispute window) ช่วงเวลาที่ผู้ถอนกด "ยังไม่ได้รับเงิน" ได้ = 72 ชั่วโมง นับจากที่ fill ถูกปิด
- ทันทีที่ปิด fill สถานะการรับเงินเป็น "รอยืนยัน" และ payload บอก
auto_confirm_atมาด้วย — ผู้ถอนไม่กดอะไรเลย ระบบยืนยันให้เองที่ 3 ชั่วโมง (webhookwithdrawal.receipt_confirmedที่confirmed_by: "auto") - 🔴 การยืนยันอัตโนมัตินี้ ไม่ใช่การอนุมัติเงิน เงินถูกปิดไปแล้วด้วยหลักฐานสลิปที่ตรวจกับธนาคาร มันแค่ปิดหน้าต่างโต้แย้งของผู้ใช้
- เปิดข้อพิพาทเมื่อไหร่ ⇒
withdrawal.disputed(funds_on_hold: true·statusไม่เปลี่ยน) และยอดฝั่งผู้ฝากถูกแช่ไว้ไม่ให้ผ่าน clearing - ตอบกลับก่อน
dispute.respond_byด้วยPOST /v1/pay/disputes/{id}/evidence· ผลออกมาเป็นwithdrawal.dispute_resolvedที่outcome ∈ confirmed | reversed
reversal — deposit.reversed / withdrawal.fill_reversed / balance.debited การดึงหรือแก้ยอดหลังจากเครดิตไปแล้ว โดย event ที่เกิดขึ้นขึ้นกับเหตุผล:
- true transfer failure (
BANK_RECALL/ACCOUNT_FROZEN/SLIP_INVALIDATED_AFTER_SETTLEMENT) อาจมีwithdrawal.fill_reversed - deposit-only correction (
DUPLICATE_CREDIT/OPS_ERROR/MERCHANT_DISPUTE_LOST) ส่งdeposit.reversed+balance.debitedเท่านั้น ไม่ส่งwithdrawal.fill_reversedและไม่เปลี่ยน withdrawal, fill หรือ fee ฝั่งถอน
🔴 event ทั้งสามชนิดปิดรับไม่ได้ ต้องมี handler ตั้งแต่วันแรก แต่ห้ามสมมติว่าทุกเหตุผลจะส่งครบทั้งสามใบ
overpayment / claim มีสองทาง: ยอดเกินในสลิปเดียวมากับ deposit.settled ใบเดียวพร้อม overpayment{cause:'same_transfer_excess'}; ส่วนการโอนเพิ่มหลัง settle มาเป็น deposit.overpaid พร้อม cause:'additional_transfer' · ทั้งคู่ คง status: 'settled' เครดิตผู้ใช้เฉพาะ amount_received_minor ซึ่งไม่เกินยอดที่สร้าง 🔴 ส่วนเกินเปิด claim ที่ status:'open' และ action_required:'contact_support' — ห้ามแตะรายการเดิม ห้ามเครดิตส่วนเกินเข้ากระเป๋า merchant และห้ามบอกว่าคืนเงินแล้ว เพราะไม่มี auto-refund
claim — POST /v1/bo/claims ช่องทางเปิดเรื่องด้วยมือ สำหรับเคสที่ระบบกู้อัตโนมัติไม่ได้ (โอนแล้วไม่เคยอัปสลิป · พ้นหน้าต่าง 24 ชั่วโมง · โอนเข้า QR เก่านอกรายการ) · ผลลัพธ์ที่เป็นไปได้: credited / refund_issued / rejected แจ้งผ่าน webhook claim.updated
สภาพคล่องและช่องทาง
liquidity_source — internal | external_gateway ที่มาของเงินฝั่งปลายทาง มีสองค่านี้เท่านั้น 🔴 ห้ามใช้ค่านี้ตัดสินใจเรื่องเครดิต — ใช้ทำสถิติหรือ debug ได้ แต่ตัวที่ตัดสินคือ status เสมอ
external gateway ช่องทางสำรองที่ใช้เมื่อไม่มีผู้ถอนในระบบมารับ · จากมุมของคุณต่างจากปกติแค่สองอย่าง: liquidity_source เป็น external_gateway และ expires_at มักสั้นกว่ามาก (~4 นาที) — flow, event, สถานะ และวิธีเครดิตเหมือนกันทุกประการ รายการที่วิ่งผ่านช่องทางนี้ ไม่มีเลขอ้างอิงของบุคคลที่สามให้ ⇒ ให้ผู้ใช้อ้างอิงด้วย deposit_id ทุกช่องทาง
suggested_amounts_minor[] ยอดที่จับคู่ได้ทันทีในขณะนั้น · มากับ 202 awaiting_liquidity ⇒ เสนอให้ผู้ใช้เลือกแทนที่จะให้รอเฉย ๆ
บัญชีของทั้งสองฝั่ง
sender / sender_id บัญชีที่ผู้ใช้ จะกดโอนจริง — ไม่ใช่ "เจ้าของ account ในระบบคุณ" · สามีโอนให้ภรรยา ⇒ ส่งบัญชีของสามี 🔴 บังคับ ไม่ส่งได้ 422 sender_required · sender_id จาก GET /v1/pay/senders การันตีว่าไม่เข้าคิวเพราะชื่อหรือเลขบัญชีไม่ตรง
beneficiary_id บัญชีผู้รับเงินถอน · ต้องผ่าน verify ชื่อกับธนาคารก่อน (webhook beneficiary.verified / beneficiary.rejected) ไม่งั้นสร้างรายการถอนได้ 409 beneficiary_not_verified และไม่มีการล็อกเงิน
ศัพท์ของ API
_minor ทุก field ที่เป็นจำนวนเงินลงท้ายด้วย _minor และเป็น integer หน่วย สตางค์ ไม่มีข้อยกเว้น · 5,000.00 บาท = 500000
version เลขที่เพิ่มขึ้นทุกครั้งที่ object เปลี่ยน · ระบบรับประกันว่า ไม่มีสอง event ของ object เดียวกันที่ version เท่ากัน ⇒ ใช้เป็น guard ใน UPDATE ... WHERE version < $new แล้วผูก side effect กับ rowCount ได้อย่างปลอดภัย
sequence เลขลำดับ event ต่อ merchant เพิ่มขึ้นตาม เวลาที่สร้าง ไม่ใช่เวลาที่ส่งถึง · ช่องว่างชั่วคราวเป็นเรื่องปกติ (event ที่ยัง retry อยู่) · ช่องว่างที่ค้างเกิน 4 วัน = หายจริง ให้ไป reconcile
event.id — evt_... ค่าเดิมทุก attempt และทุกครั้งที่สั่งส่งซ้ำด้วยมือ ⇒ ใช้เป็น unique key ใน transactional inbox · เก็บ ≥ 31 วัน 🔴 เจอซ้ำต้องตอบ 200 ไม่ใช่ 409
livemodefalse = sandbox · true = live · event สองฝั่งข้ามกันไม่ได้เด็ดขาด ⇒ handler ต้องตรวจว่าตรงกับ environment ตัวเอง
api_version รูปแบบ YYYY-MM-DD ที่คุณ pin ไว้ · การเพิ่ม field ใหม่และการเพิ่มค่าใหม่ใน enum ที่ประกาศว่า "open" ไม่นับเป็น breaking change ⇒ 🔴 ทุก switch ของ status, reasons[].code, balance_transaction.type, fee_breakdown[].component ต้องมี default:
canonical string สตริง 9 บรรทัดที่ใช้เป็น input ของลายเซ็น HMAC-SHA256 บน request ขาออกของคุณ — ดู authentication
Idempotency-Key กับ PG-Nonce สองตัวนี้ทำหน้าที่ตรงข้ามกัน · PG-Nonce ต้อง ใหม่ทุก attempt · Idempotency-Key ต้อง คงเดิมตลอด logical operation
magic amount ยอดพิเศษ 100.00–116.00 บาทบน sandbox ที่ใช้บังคับให้รายการเดินเส้นทางที่ต้องการ — ดู sandbox
คำที่ระบบนี้ไม่มี และจะไม่มี
| คำที่คุณอาจคุ้นจาก gateway เจ้าอื่น | ของเราคืออะไร |
|---|---|
| เศษสตางค์เพื่อจับคู่รายการ | จับคู่ด้วย ตัวตนของผู้โอน ยอดเป็นเลขกลมเสมอ |
payment_instructions[] / bank_account / promptpay | legs[] |
409 duplicate_slip แบบตอบทันที | durable intake สำเร็จตอบ 202; ผลตรวจซ้ำมาทีหลังทาง webhook |
callback envelope {code, message, result} ที่คืน HTTP 200 เสมอ | HTTP status จริง (201 202 402 409 410 422 429 503) |
| ยอดเข้ากระเป๋าตัวเลขเดียว | 4 ยอดหลัก ordered_amount_minor (X) / transferred_amount_minor (V) / amount_received_minor (A) / overpayment.amount_minor (E, หรือ 0) และแยก fee_total_minor / credited_amount_minor เพิ่ม |
| "callback ครั้งเดียวจบ" | ลำดับไม่การันตี · retry 11 ครั้งกินเวลา ~92 ชั่วโมง · ต้องมี version guard |
| อนุมัติอัตโนมัติเมื่อ SLA หมด | ไม่มี — มีแต่ review.escalated |
ขั้นต่อไป
- ยังไม่เห็นภาพว่าเงินวิ่งทางไหน → ภาพรวมระบบ
- อยากเห็นของจริงวิ่ง → Quickstart
- จะจำลองเคสที่ศัพท์เหล่านี้โผล่มา → Sandbox
- ชื่อ field ตัวไหนใช้ตัวไหนเลิกใช้ → พจนานุกรม field
