พจนานุกรม field
หน้านี้คือรายชื่อฟิลด์ทั้งหมดที่คุณจะเจอ ใช้เป็นที่อ้างอิงตอนเขียน schema ฝั่งคุณ
กติกา 6 ข้อที่ใช้กับทุกฟิลด์
| # | กติกา | ตัวอย่าง |
|---|---|---|
| 1 | ฟิลด์จำนวนเงินลงท้าย _minor ทุกตัว ไม่มีข้อยกเว้น และเป็น integer หน่วยสตางค์ | 5,000.00 บาท = 500000 · 75.00 บาท = 7500 |
| 2 | เวลาเป็น RFC3339 UTC ลงท้าย Z มิลลิวินาที 3 หลักเสมอ | "2026-08-21T04:12:33.881Z" · 🔴 ไม่มี offset ไทยในทุก payload |
| 3 | "มีเสมอ" = รับประกันว่ามี key · optional = อาจไม่มี key นั้นเลย ไม่ใช่ null | if ('legs' in obj) ปลอดภัยกว่า obj.legs !== null |
| 4 | ฟิลด์ที่เป็น null ได้ จะระบุไว้ชัดในคอลัมน์ "มีเสมอ" | late_slip_accepted_until: null แปลว่าไม่มีหน้าต่างอัปสลิปย้อนหลัง |
| 5 | open enum เพิ่มค่าใหม่ได้โดยไม่นับเป็น breaking change ⇒ ทุก switch ต้องมี default: | reasons[].code, rejection.code, balance_transaction.type, ประเภทค่าธรรมเนียม |
| 6 | ตัวเลขนับ (leg_count, settled_splits, attempt) เป็น integer ธรรมดา ไม่ใช่ _minor | leg_count: 2 |
เลข minor ในภาษาที่ float ทำร้ายคุณได้
amount_minor เป็น integer ⇒ ห้ามแปลงเป็น float ทุกกรณี · JavaScript ปลอดภัยถึง 9,007,199,254,740,991 สตางค์ (~90 ล้านล้านบาท) แต่ ห้ามใช้ parseFloat แล้วคูณ 100 กลับไปกลับมา
PHP: ใช้ int และ intdiv() ไม่ใช่ / · MySQL/Postgres: คอลัมน์ต้องเป็น BIGINT ไม่ใช่ DECIMAL และไม่ใช่ INT (ยอดสะสมรายเดือนของ merchant ขนาดกลางล้น INT ภายในปีเดียว)
🔴 สี่ยอด X/V/A/E ที่สับสนกันบ่อยที่สุด
นี่คือจุดที่ทำให้ merchant เสียเงินจริงมากที่สุดในเอกสารทั้งชุด อ่านให้จบก่อนเขียนโค้ดเครดิต
| ฟิลด์ | แปลว่าอะไร | เอาไปทำอะไร |
|---|---|---|
ordered_amount_minor (X) | ยอดที่ สั่งไว้ ตอนสร้างรายการ | ใช้เทียบว่าโอนครบไหม · แสดงในใบเสร็จ |
transferred_amount_minor (V) | ยอดที่ธนาคารยืนยันว่า โอนจริง | ตรวจสอบ exact/short/over; ห้ามใช้เครดิตโดยตรงเมื่อ overpay |
amount_received_minor (A) | ยอดที่ระบบ รับเข้า order นี้ | 🔴 เครดิตให้ผู้ใช้ปลายทางด้วยตัวนี้ · ไม่เกินยอดที่สั่ง |
overpayment.amount_minor (E) | ยอดส่วนเกิน; ไม่มี/ถือเป็น 0 เมื่อไม่ overpay | เปิด claim ให้ผู้ใช้ติดต่อ support · ห้ามเครดิตหรือคืนอัตโนมัติ |
ค่าธรรมเนียมและยอดกระเป๋าเป็นอีกสองค่าคนละหน้าที่: fee_total_minor และ credited_amount_minor โดยความสัมพันธ์นี้เป็นจริงทุกใบ ไม่มีข้อยกเว้น
0 < 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 overpay: transferred_amount_minor = amount_received_minor + overpayment.amount_minortransfer เพิ่มหลังรายการ settled (D17) สร้าง claim/flag ใหม่ แต่ไม่แก้ transferred_amount_minor หรือ amount_received_minor ของ settlement evidence เดิม
ตัวอย่างจริง — ผู้ใช้เติม 5,000.00 บาท ค่าธรรมเนียม 75.00 บาท
{
"ordered_amount_minor": 500000,
"transferred_amount_minor": 500000,
"amount_received_minor": 500000,
"fee_total_minor": 7500,
"credited_amount_minor": 492500,
"partial": false,
"settled_via": "auto"
}- ผู้ใช้ปลายทางต้องได้ 5,000.00 บาท (
amount_received_minor) — exact จึงเท่ากับยอดโอนจริง; ค่าธรรมเนียมเป็นของ merchant - กระเป๋า merchant ขยับ +4,925.00 บาท (
credited_amount_minor)
ตัวอย่างเคสโอนขาดที่ผิดเฉพาะยอด — สั่ง 5,000.00 โอนจริง 3,000.00 และ non-amount อีก 5 มิติผ่าน ระบบรับยอดจริงอัตโนมัติ
{
"ordered_amount_minor": 500000,
"transferred_amount_minor": 300000,
"amount_received_minor": 300000,
"fee_total_minor": 4500,
"credited_amount_minor": 295500,
"partial": true,
"settled_via": "auto"
}ตัวอย่างโอนเกิน — สั่ง 5,000.00 แต่ธนาคารยืนยัน 5,300.00; รายการสำเร็จทันทีที่ 5,000.00
{
"ordered_amount_minor": 500000,
"transferred_amount_minor": 530000,
"amount_received_minor": 500000,
"fee_total_minor": 7500,
"credited_amount_minor": 492500,
"partial": false,
"overpayment": {
"claim_id": "clm_01K3PB6QW1M8HN0DZ2XKQ7RV4T",
"cause": "same_transfer_excess",
"amount_minor": 30000,
"slip_received_at": "2026-08-21T04:19:02.180Z",
"status": "open",
"action_required": "contact_support"
}
}เครดิตผู้ใช้และคิด fee เฉพาะ 500000; แสดงเลข claim และให้ติดต่อ support เรื่อง 30000 โดยไม่รอให้รายการฝากสำเร็จและไม่คืนอัตโนมัติ
สองความผิดพลาดที่ตรงข้ามกัน
เครดิตผู้ใช้ด้วย credited_amount_minor ⇒ ผู้ใช้ได้เงินขาดเท่ากับค่าธรรมเนียมทุกออร์เดอร์ ⇒ ลูกค้าร้องเรียนตั้งแต่วันแรก ขยับกระเป๋าด้วย amount_received_minor ⇒ ยอดในระบบคุณสูงกว่าความจริงเท่ากับค่าธรรมเนียมทั้งเดือน ⇒ รู้ตอนกระทบยอดสิ้นเดือนแล้วตามแก้ไม่ทัน
ทางกัน: เก็บ X/V/A/E แยกกัน และเก็บ fee/merchant credit แยกเพิ่ม แล้ว assert ทีละใบ ใน job รายวันว่า A = min(V,X), amount_received_minor − fee_total_minor = credited_amount_minor และกรณี same-slip overpay ให้ V = A + E
credited_amount_minor ยังถอนไม่ได้ทันที
ยอดที่เพิ่งเครดิตเข้าไปอยู่ในถัง pending_clearing ก่อน แล้วย้ายไป available ตอน clearing_available_at (ปกติ T+72 ชั่วโมง) — คุณจะได้ event balance.cleared ตอนนั้น; ถ้าเงินถูก hold ค่าเวลานี้เป็น null จนกว่าจะระบุเวลาปล่อยได้
🔴 balance.cleared คือ การย้ายถัง ไม่ใช่เงินเข้าใหม่ ⇒ ห้ามบวกยอดซ้ำ · สร้างรายการถอนก่อนถึงเวลานั้นจะได้ 402 insufficient_balance
ซองของทุก event (webhook envelope)
โครงนี้เหมือนกันทุก event ไม่มีข้อยกเว้น
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
id | string (evt_ + ULID) | ✅ | id ของ event · ไม่ซ้ำตลอดกาล · ค่าเดิมทุก attempt และทุกครั้งที่สั่งส่งซ้ำ ⇒ ใช้เป็น unique key กันซ้ำได้ |
object | string | ✅ | ค่าคงที่ "event" |
type | string (open enum) | ✅ | ชื่อ event เช่น deposit.settled · 🔴 บอกแค่ "มีอะไรเกิดขึ้น" ห้ามใช้ตัดสินใจเรื่องเงิน |
api_version | string YYYY-MM-DD | ✅ | เวอร์ชันที่คุณตรึงไว้ ณ ตอนสร้าง event |
livemode | boolean | ✅ | false = sandbox · 🔴 event ของคนละโหมดไม่มีทางข้ามกัน — ตรวจให้ตรง environment |
created_at | string RFC3339 UTC | ✅ | เวลาที่ สร้าง event ไม่เปลี่ยนตอน retry |
sequence | integer (int64) | ✅ | เพิ่มขึ้นเสมอต่อ merchant · ใช้ตรวจว่ามี event หายไหม · ช่องว่างชั่วคราวเป็นเรื่องปกติ (ของที่ยัง retry อยู่) · ช่องว่างที่ค้างเกิน 4 วัน = หายจริง |
account.merchant_id | string (mch_ + ULID) | ✅ | บัญชี merchant ที่ event นี้เป็นของ |
account.white_label_id | string (wl_ + ULID) | ✅ | แบรนด์ที่ merchant นี้สังกัด |
request.id | string (req_ + ULID) | optional | มีเมื่อ event เกิดจากการเรียก API · ตรงกับ request_id ในตัว error body |
request.idempotency_key | string | optional | key ที่คุณส่งมาตอนสร้าง |
data.object | object | ✅ | business object เต็มใบ ณ ขณะนั้น ⇒ ไม่ต้อง GET กลับมาถาม |
data.previous_attributes | object | null | ✅ (ยกเว้น object ที่แก้ไม่ได้) | เฉพาะ key ที่เปลี่ยนจริง + version เดิม · null สำหรับ event *.created · ไม่มี key นี้เลย สำหรับ balance_transaction |
version คือหัวใจของการเขียนสถานะลง DB
data.object.version เป็น integer ที่เพิ่มทุกครั้งที่ object เปลี่ยน และ ไม่มีทางที่ event สองใบของ object เดียวกันจะมี version เท่ากัน
⇒ เขียนลง DB ด้วย UPDATE … WHERE id = $1 AND version < $new แล้วเช็ค rowCount · rowCount = 0 แปลว่า event เก่ามาทีหลัง ให้ตอบ 200 แล้วไม่ทำอะไร · ดู version guard
object deposit_order
แกนหลัก — มีในทุก event ฝั่งฝาก
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
id | string (dep_ + ULID) | — | ✅ | id ของรายการฝาก · 🔴 ห้าม parse ห้ามเดา prefix · คอลัมน์ฝั่งคุณต้องเป็น text ไม่ใช่ varchar(20) |
object | string | — | ✅ | ค่าคงที่ "deposit_order" |
version | integer | — | ✅ | ตัวนับการเปลี่ยนแปลง ใช้ทำ version guard |
status | string (closed enum 9 ค่า) | — | ✅ | สถานะ ดู State machine |
is_terminal | boolean | — | ✅ | 🔴 จบจริงหรือยัง — ผูกกับ late_slip_accepted_until ไม่ใช่กับชื่อสถานะ |
merchant_reference | string ≤ 64 | — | ✅ | เลขอ้างอิงฝั่งคุณที่ส่งมาตอนสร้าง |
amount_minor | integer | สตางค์ | ✅ | ยอดที่สั่งไว้ตอนสร้างรายการ |
currency | string | — | ✅ | ปัจจุบัน "THB" เท่านั้น |
created_at | string RFC3339 UTC | — | ✅ | เวลาที่สร้างรายการ |
expires_at | string RFC3339 UTC | — | ✅ | 🔴 หมดเวลาเมื่อไหร่ — ใช้ค่านี้เสมอ ห้าม hardcode ค่าจริงอาจสั้นเพียง ~4 นาที และ เปลี่ยนได้ เมื่อรายการเลื่อนจาก awaiting_liquidity ไป awaiting_payment |
checkout_url | string URL | — | optional | หน้าชำระเงินสำเร็จรูปบนโดเมนแบรนด์ · เป็นทางที่ปลอดภัยกว่าและเป็นค่าที่แนะนำ |
liquidity_source | "internal" | "external_gateway" | — | optional | ที่มาของช่องทาง · 🔴 ห้ามใช้ตัดสินใจเครดิต ใช้ทำรายงานเท่านั้น |
leg_count | integer | — | optional | จำนวนปลายทางที่ต้องโอน · > 1 ได้ |
legs[] | array | — | optional | รายการปลายทาง ดูตารางถัดไป · ว่าง [] ตอน awaiting_liquidity |
sender | object | — | ✅ | บัญชีผู้โอนที่คุณแจ้งไว้ (ถูก mask แล้ว) |
metadata | object ≤ 20 key | — | optional | ค่าที่คุณส่งมาเอง เราคืนกลับเหมือนเดิม · ค่าละ ≤ 500 ไบต์ |
legs[] — ปลายทางที่ผู้ใช้ต้องโอนไป
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
leg_ref | string | — | ✅ | id ของก้อนนี้ที่คุณเห็น · 🔴 ฝั่งผู้ถอนได้ค่าคนละตัว จับคู่ข้าม merchant ไม่ได้โดยตั้งใจ |
sequence | integer | — | ✅ | ลำดับที่เท่าไหร่ (1, 2, …) ใช้ทำ "ขั้นที่ 1 จาก 2" |
amount_minor | integer | สตางค์ | ✅ | ยอดของก้อนนี้ · ผลรวมทุก leg = amount_minor ของรายการ |
expires_at | string RFC3339 UTC | — | ✅ | หมดอายุของก้อนนี้ |
leg_status | "pending" | "complete" | — | ✅ | 🔴 ไม่ใช่สถานะเงิน ใช้ทำ progress เท่านั้น |
method | string | — | ✅ | ช่องทาง เช่น "promptpay_qr" |
qr_payload | string | — | optional | สตริง QR ดิบ · 🔴 ห้าม log ห้ามเก็บลง DB ห้ามส่งต่อ ใช้แสดงผลแล้วทิ้ง · แบรนด์ปิดการส่งฟิลด์นี้ได้ |
qr_image_url | string URL | — | optional | ภาพ QR บนโดเมนของแบรนด์ |
receiver.bank_code | string 3 หลัก | — | ✅ | รหัสธนาคารปลายทาง |
receiver.bank_name_th | string | — | ✅ | ชื่อธนาคารภาษาไทย |
receiver.account_number_masked | string | — | ✅ | เลขบัญชีปลายทางแบบปิดบัง เช่น "xxx-x-x1234" · 🔴 ไม่มีเลขบัญชีเต็มให้ทุกกรณี |
receiver.account_name_masked | string | — | ✅ | ชื่อผู้รับแบบปิดบัง เช่น "ส*** ใ***" |
{
"leg_count": 2,
"legs": [
{
"leg_ref": "leg_7QF2H8KM",
"sequence": 1,
"amount_minor": 300000,
"expires_at": "2026-08-21T04:14:41.000Z",
"leg_status": "pending",
"method": "promptpay_qr",
"qr_image_url": "https://pay.example-wl.com/qr/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/1.png",
"receiver": {
"bank_code": "014",
"bank_name_th": "ธนาคารไทยพาณิชย์",
"account_number_masked": "xxx-x-x1234",
"account_name_masked": "ส*** ใ***"
}
},
{
"leg_ref": "leg_7QF2H8KN",
"sequence": 2,
"amount_minor": 200000,
"expires_at": "2026-08-21T04:14:41.000Z",
"leg_status": "pending",
"method": "promptpay_qr",
"qr_image_url": "https://pay.example-wl.com/qr/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/2.png",
"receiver": {
"bank_code": "004",
"bank_name_th": "ธนาคารกสิกรไทย",
"account_number_masked": "xxx-x-x9887",
"account_name_masked": "ว*** ป***"
}
}
]
}legs[0] ไม่ใช่ทั้งหมด
รายการเดียวถูกซอยเป็นหลายปลายทางได้ ⇒ UI ต้องรองรับ "ขั้นที่ 1 จาก 2" · โค้ดที่อ่าน legs[0].qr_payload แล้วจบ จะทำให้ผู้ใช้โอนไม่ครบ แล้วรายการเข้าคิวตรวจสอบทั้งหมด
sender — บัญชีผู้โอนที่คุณแจ้งไว้
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
sender.bank_code | string 3 หลัก | ✅ | ธนาคารต้นทาง |
sender.account_number_masked | string | ✅ | เลขบัญชีที่คุณแจ้ง แสดงแบบปิดบัง |
sender.account_name_th_masked | string | ✅ | ชื่อที่คุณแจ้ง แสดงแบบปิดบัง |
🔴 เราไม่ส่งชื่อหรือเลขบัญชีที่อ่านได้จากสลิปจริงกลับไปให้เลย — เพราะคนที่โอนจริงอาจไม่ใช่คนเดียวกับที่คุณแจ้ง (เช่นโอนจากบัญชีคู่สมรส) และนั่นเป็นข้อมูลส่วนบุคคลของบุคคลที่สาม · สิ่งที่ได้คือผลเทียบแบบ boolean ใน verification{}
verification{} — ผลตรวจ 6 มิติ (boolean ล้วน)
| ฟิลด์ | ชนิด | ความหมาย |
|---|---|---|
verification.amount_exact | boolean | ยอดตรงเป๊ะ (ไม่มี tolerance) |
verification.within_time_window | boolean | เวลาโอนอยู่ในช่วงของรายการ |
verification.sender_bank_match | boolean | ธนาคารต้นทางตรง |
verification.sender_account_match | boolean | เลขบัญชีต้นทางตรง |
verification.sender_name_match | boolean | ชื่อบัญชีต้นทางตรง |
verification.receiver_match | boolean | ปลายทางตรง |
ยอดตรงต้องครบ 6 ข้อจึงเป็น exact · เมื่อ non-amount อีก 5 ข้อผ่าน short-pay จะ settle อัตโนมัติที่ A=V<X และ same-slip overpay จะ settle ที่ยอด order พร้อม claim · มีเหตุ non-amount/ambiguity/policy อื่นจริงจึงเข้า under_review ไม่ใช่ปฏิเสธ
review{} — มีเมื่อ status = "under_review"
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
review.opened_at | string RFC3339 UTC | ✅ | เวลาที่เข้าคิว |
review.escalated | boolean | ✅ | ถูกยกระดับแล้วหรือยัง · 🔴 การยกระดับ ไม่แตะเงินและไม่เปลี่ยนสถานะ แต่ version ขยับ |
review.four_eyes_required | boolean | ✅ | ต้องมีผู้อนุมัติสองคน ⇒ บอกผู้ใช้ได้ว่า "อาจใช้เวลานานขึ้น" |
review.estimated_resolution_bucket | "under_15m" | "under_1h" | "under_4h" | "extended" | ✅ | ช่วงเวลาโดยประมาณ · เอาไปแสดงให้ผู้ใช้ได้ตรง ๆ |
review.reasons[] | array | ✅ | เหตุผล เรียงตามความสำคัญ · reasons[0] คือตัวที่ควรแสดงถ้าจอมีที่เดียว |
review.reasons[] แต่ละตัว
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
code | string (open enum) | ✅ | รหัสเหตุผล เช่น SENDER_NAME_MISMATCH |
display_th | string | ✅ | 🔴 ข้อความไทยที่เราเขียนมาให้แล้ว — เอาไปแสดงได้ทันที ไม่ต้องเขียนเอง |
display_en | string | ✅ | ข้อความอังกฤษ |
actionable_by | "end_user" | "support" | ✅ | ใครแก้ได้ · end_user = ผู้ใช้ทำอะไรได้ · support = รอทีมงานอย่างเดียว |
detail | object | optional | รายละเอียดเพิ่ม ต่างกันตาม code · 🔴 RECEIVER_MISMATCH ไม่มี detail เด็ดขาด |
slip{} และ liquidity{}
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
slip.received_at | string RFC3339 UTC | optional | เวลาที่ระบบรับสลิป |
slip.is_late | boolean | optional | true = เป็นสลิปที่อัปย้อนหลังหลังรายการหมดอายุหรือถูกยกเลิก |
liquidity.wait_started_at | string RFC3339 UTC | optional | เริ่มรอตั้งแต่เมื่อไหร่ (มีตอน awaiting_liquidity) |
liquidity.estimated_wait_ms | integer (มิลลิวินาที) | optional | ประมาณการเวลารอ |
liquidity.retry_interval_ms | integer (มิลลิวินาที) | optional | ระบบพยายามจับคู่ใหม่ทุกกี่มิลลิวินาที |
liquidity.suggested_amounts_minor[] | array of integer | สตางค์ | ยอดที่จับคู่ได้ทันทีถ้าผู้ใช้ยอมเปลี่ยน ⇒ เสนอเป็นปุ่มให้กด |
ฟิลด์ที่โผล่มาตอน settled
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
ordered_amount_minor | integer | สตางค์ | ✅ | ยอดที่สั่งไว้ตอนสร้าง |
transferred_amount_minor | integer | สตางค์ | ✅ | ยอดที่ธนาคารยืนยันว่าโอนจริง (V) |
amount_received_minor | integer | สตางค์ | ✅ | 🔴 ยอดที่ระบบรับเข้า order (A) — เครดิตผู้ใช้ด้วยตัวนี้ |
overpayment | object | null | — | required เมื่อ settled/reversed | flag/claim ของ E; เป็น object เมื่อโอนเกิน มิฉะนั้นเป็น null · ห้ามละ key ใน terminal financial snapshot |
fee_total_minor | integer | สตางค์ | ✅ | ค่าธรรมเนียมรวมที่เก็บจาก merchant |
credited_amount_minor | integer | สตางค์ | ✅ | 🔴 ยอดเข้ากระเป๋า merchant — ขยับกระเป๋าด้วยตัวนี้ |
partial | boolean | — | ✅ | true = รับ short-pay ตามยอดจริง (A=V<X); ปกติ settled_via:'auto' เมื่อ non-amount 5/5 ผ่าน และเป็น 'review' เฉพาะเคสที่มีเหตุอื่นแล้วเจ้าหน้าที่อนุมัติ · same-slip overpay เป็น false และมี overpayment{} |
settled_via | "auto" | "review" | — | ✅ | ผ่านการตรวจอัตโนมัติ หรือผ่านการตัดสินของเจ้าหน้าที่ |
balance_transaction_id | string (btxn_ + ULID) | — | ✅ | รายการเคลื่อนไหวยอดคงเหลือที่คู่กัน |
clearing_available_at | string RFC3339 UTC | null | — | ✅ | เวลาที่ยอดนี้ย้ายไปถัง available; เป็น null ขณะที่เงินถูก hold และยังระบุเวลาปล่อยไม่ได้ |
fee_breakdown[] | array | — | ✅ | รายละเอียดค่าธรรมเนียม ดูด้านล่าง |
fee_breakdown[] — แยกตามประเภทค่าธรรมเนียม
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
component | string (open enum) | — | ✅ | ประเภท: "processing_fee" | "fixed_fee" | "vat" |
amount_minor | integer | สตางค์ | ✅ | จำนวนของประเภทนี้ |
rate_bps | integer | basis point | optional | อัตราคิด เช่น 150 = 1.50% (มีเฉพาะประเภทที่คิดเป็นอัตรา) |
{
"fee_total_minor": 7500,
"fee_breakdown": [
{ "component": "processing_fee", "amount_minor": 6500, "rate_bps": 130 },
{ "component": "fixed_fee", "amount_minor": 1000 }
]
}🔴 แยกตามประเภทเท่านั้น ไม่มีการแยกว่าเงินก้อนนี้ไปถึงใคร ⇒ เวลาโต้แย้งเรื่องค่าธรรมเนียม ให้เก็บ array นี้ดิบ ๆ ไว้ และ parse ด้วย component เสมอ · component เป็น open enum ⇒ ต้องมี default:
ฟิลด์ตอนหมดเวลา ยกเลิก ปฏิเสธ กลับรายการ และโอนซ้ำ
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
late_slip_accepted_until | string RFC3339 UTC | null | ✅ ในเหตุการณ์หมดเวลา/ยกเลิก | 🔴 นาฬิกาเดียวของหน้าต่างอัปสลิปย้อนหลัง · null = ไม่เคยมีปลายทางให้โอน ⇒ ปิดออร์เดอร์ได้เลย |
expired_reason | string | optional | ปัจจุบันมีค่าเดียว "no_payment" |
canceled_by | "merchant" | "end_user" | "system" | optional | ใครเป็นคนยกเลิก |
cancel_reason_code | string (open enum) | optional | "no_liquidity_available" | "merchant_request" | "end_user_request" | "approval_ttl_expired" |
rejection.code | string (open enum) | optional | รหัสเหตุผลที่ไม่รับสลิป |
rejection.display_th / display_en | string | optional | ข้อความที่เอาไปแสดงได้ทันที · 🔴 เราไม่ส่งบันทึกภายในของเจ้าหน้าที่ออกมา |
reversal.reason_code | string (open enum) | optional | "BANK_RECALL" | "ACCOUNT_FROZEN" | "SLIP_INVALIDATED_AFTER_SETTLEMENT" | "DUPLICATE_CREDIT" | "OPS_ERROR" | "MERCHANT_DISPUTE_LOST" |
reversal.reverses_balance_transaction_id | string (btxn_) | optional | ชี้ไปรายการเงินเดิมที่ถูกกลับ ⇒ กลับรายการฝั่งคุณได้แม่นยำ |
reversal.balance_transaction_id | string (btxn_) | optional | รายการเงินของการกลับรายการเอง |
overpayment.claim_id | string | ✅ เมื่อ overpayment เป็น object | เลขเรื่องของยอดเกิน |
overpayment.cause | same_transfer_excess | additional_transfer | ✅ เมื่อ overpayment เป็น object | เกินในสลิปเดียวหรือมี transfer เพิ่ม |
overpayment.amount_minor | integer สตางค์ | ✅ เมื่อ overpayment เป็น object | ยอดส่วนเกิน |
overpayment.slip_received_at | string RFC3339 UTC | ✅ เมื่อ overpayment เป็น object | เวลาที่ได้รับสลิปที่ทำให้เกิด flag |
overpayment.status | open | ✅ เมื่อ overpayment เป็น object | immutable snapshot ตอนเปิดเรื่อง; สถานะล่าสุดอ่าน Claim.version |
overpayment.action_required | contact_support | ✅ เมื่อ overpayment เป็น object | ให้ผู้ใช้ติดต่อ support พร้อม claim id · ไม่มี auto-refund |
overpayment.expected_response_at | string RFC3339 UTC | optional | เวลาตอบครั้งแรก ถ้ากำหนดแล้ว |
overpayment{} มาแล้ว status ยังเป็น settled
same-slip overpay มากับ deposit.settled ใบเดียว โดยเครดิตไม่เกินยอดที่สร้าง; transfer เพิ่มภายหลังมาเป็น deposit.overpaid. ทั้งสองกรณีเปิด claim และ data.object.status ยังเป็น "settled" — ให้ผู้ใช้ติดต่อ support, อย่าบอกว่าคืนเงินแล้ว
⇒ โค้ดที่ทำ if (event.type === 'deposit.overpaid') markFailed() จะยกเลิกออร์เดอร์ที่สำเร็จไปแล้ว
object withdrawal_order
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
id | string (wdr_ + ULID) | — | ✅ | id ของรายการถอน |
object | string | — | ✅ | ค่าคงที่ "withdrawal_order" |
version | integer | — | ✅ | ตัวนับการเปลี่ยนแปลง |
status | string (closed enum 7 ค่า) | — | ✅ | สถานะ ดู State machine |
merchant_reference | string ≤ 64 | — | ✅ | เลขอ้างอิงฝั่งคุณ |
amount_total_minor | integer | สตางค์ | ✅ | ยอดถอนที่สั่งไว้ |
fee_total_minor | integer | สตางค์ | ✅ | ค่าธรรมเนียมรวม ที่ล็อกไว้ตั้งแต่ตอนสร้าง · 🔴 ฝั่งถอนค่าธรรมเนียม บวกเพิ่ม ไม่ได้หักจากยอดถอน |
total_locked_minor | integer | สตางค์ | ✅ ตอนสร้าง | ยอดที่ถูกล็อกจริง = amount_total_minor + fee_total_minor |
fee_recognized_minor | integer | สตางค์ | ✅ | ค่าธรรมเนียมที่ถูกใช้ไปแล้วตามสัดส่วนที่จ่ายออก |
filled_minor | integer | สตางค์ | ✅ | 🔴 ยอดที่จ่ายออกไปแล้วจริง — แหล่งความจริงเดียว |
remaining_minor | integer | สตางค์ | ✅ | 🔴 ยอดที่ยังเหลือ — แหล่งความจริงเดียว |
refunded_minor | integer | สตางค์ | ✅ ตอนปิดใบ | ยอดที่คืนกลับเข้ายอดคงเหลือจริง |
settled_splits | integer | — | ✅ | จำนวนก้อนที่ปิดไปแล้ว |
max_splits | integer | — | ✅ | ซอยได้สูงสุดกี่ก้อน |
expires_at | string RFC3339 UTC | — | ✅ | หลังเวลานี้ใบจะหลุดจากคิวจับคู่ |
fills[] | array | — | optional | รายละเอียดก้อนที่ปิดแล้ว (มีใน GET) |
dispute | object | — | optional | มีเมื่อผู้ถอนแจ้งว่ายังไม่ได้รับเงิน |
refund_of_deposit_id | string (dep_) | — | optional | มีเมื่อใบนี้เป็นการคืนเงินให้ผู้ใช้ (การคืนเงินคือรายการถอนประเภทหนึ่ง) |
replacement_of_withdrawal_id | string (wdr_) | — | optional | มีเมื่อใบนี้ถูกเปิดใหม่แทนใบเดิมที่ก้อนถูกกลับรายการ |
filled_minor / remaining_minor ห้ามคำนวณเอง
คุณจะได้ withdrawal.filled หลายครั้งต่อหนึ่งใบ และลำดับที่มาถึงไม่การันตี
⇒ ห้ามเขียน remaining += x และห้าม SUM() จากตาราง event ฝั่งคุณ · ให้เขียนทับด้วยค่า filled_minor / remaining_minor จาก payload ผ่าน version guard เท่านั้น · ตารางก้อนย่อยฝั่งคุณมีไว้ทำ timeline และตรวจสอบย้อนหลัง ไม่ใช่ไว้ขับตรรกะเรื่องเงิน
fills[] และ fill{} ใน event withdrawal.filled
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
fill.leg_ref | string | — | ✅ | 🔴 id ของก้อนนี้ ใช้เป็น unique key ของบรรทัด fill ฝั่งคุณ |
fill.sequence | integer | — | ✅ | ก้อนที่เท่าไหร่ |
fill.amount_minor | integer | สตางค์ | ✅ | ยอดของก้อนนี้ |
fill.fee_recognized_minor | integer | สตางค์ | ✅ | ค่าธรรมเนียมที่ถูกใช้ไปกับก้อนนี้ |
fill.settled_at | string RFC3339 UTC | — | ✅ | เวลาที่ก้อนนี้ปิด |
fill.balance_transaction_id | string (btxn_) | — | ✅ | รายการเคลื่อนไหวยอดคงเหลือที่คู่กัน |
fill.auto_confirm_at | string RFC3339 UTC | — | ✅ | ถ้าผู้ถอนไม่กดอะไร ระบบจะถือว่ารับเงินแล้วเมื่อถึงเวลานี้ (3 ชั่วโมง) |
fill.receipt_state | string | — | optional (ใน GET) | สถานะการยืนยันรับเงินของก้อนนี้ |
วิธีเก็บบรรทัด fill ที่ทนต่อการสลับลำดับ
- บรรทัด fill:
INSERT … ON CONFLICT (leg_ref) DO NOTHING—leg_refไม่ซ้ำ ⇒ ได้ event เดิมซ้ำก็ไม่เพิ่มบรรทัด - ยอดรวมของใบ:
UPDATE … SET filled_minor = $x, remaining_minor = $y WHERE id = $1 AND version < $v— เขียนทับเฉพาะเมื่อversionสูงกว่า
สองอย่างนี้ทำคนละหน้าที่ ต้องมีทั้งคู่ · ดู หนึ่งใบรับได้หลายก้อน
dispute{}
| ฟิลด์ | ชนิด | มีเสมอ | ความหมาย |
|---|---|---|---|
dispute.id | string | ✅ | เลขข้อพิพาท |
dispute.reason_code | string (open enum) | ✅ | เหตุผลที่เปิด |
dispute.amount_minor | integer สตางค์ | ✅ | ยอดที่พิพาท |
dispute.opened_at | string RFC3339 UTC | ✅ | เปิดเมื่อไหร่ |
dispute.respond_by | string RFC3339 UTC | ✅ | 🔴 ต้องส่งหลักฐานก่อนเวลานี้ ผ่าน POST /v1/pay/disputes/{id}/evidence |
dispute.evidence_url | string URL | optional | ลิงก์หน้าส่งหลักฐาน |
funds_on_hold | boolean | ✅ | true = ยอดที่เกี่ยวข้องถูกพักไว้ชั่วคราว · status ของใบ ไม่เปลี่ยน |
object balance_transaction
object นี้ แก้ไม่ได้ ⇒ ไม่มี version และไม่มี previous_attributes ⇒ กันซ้ำด้วย id แล้ว สะสม ไม่ใช่เขียนทับ
| ฟิลด์ | ชนิด | หน่วย | มีเสมอ | ความหมาย |
|---|---|---|---|---|
id | string (btxn_ + ULID) | — | ✅ | 🔴 unique key สำหรับกันบันทึกซ้ำ |
object | string | — | ✅ | ค่าคงที่ "balance_transaction" |
type | string (open enum) | — | ✅ | "deposit_settled" | "clearing_released" | "withdrawal_reserved" | "withdrawal_settled" | "withdrawal_released" | "fee" | "reversal" | "adjustment" |
direction | "credit" | "debit" | — | ✅ | ทิศทาง |
amount_minor | integer | สตางค์ | ✅ | จำนวน (ค่าบวกเสมอ ทิศทางอยู่ที่ direction) |
currency | string | — | ✅ | "THB" |
bucket_from | "available" | "pending_clearing" | "reserved" | null | — | ✅ | ย้ายออกจากถังไหน |
bucket_to | เหมือน bucket_from | — | ✅ | ย้ายเข้าถังไหน |
available_at | string RFC3339 UTC | — | optional | ยอดนี้จะถอนได้เมื่อไหร่ |
balance_after | object | — | ✅ | ยอดคงเหลือ 3 ถังหลังรายการนี้ · 🔴 ใช้ตรวจสอบเท่านั้น ห้ามใช้เขียนทับยอดในระบบคุณ (event สลับลำดับได้) |
source.type / source.id | string | — | ✅ | ต้นทาง เช่น {"type":"deposit","id":"dep_…"} |
reverses_balance_transaction_id | string (btxn_) | — | optional | มีเฉพาะรายการกลับรายการ |
reason_code / reason_display_th | string | — | optional | เหตุผลของการกลับรายการ |
created_at | string RFC3339 UTC | — | ✅ | เวลาที่เกิดรายการ |
{
"id": "btxn_01K3PB6QW1M8HN0DZ2XKQ7RV4T",
"object": "balance_transaction",
"type": "reversal",
"direction": "debit",
"amount_minor": 492500,
"currency": "THB",
"bucket_from": "pending_clearing",
"bucket_to": null,
"balance_after": { "available_minor": 1284300, "pending_clearing_minor": 0, "reserved_minor": 200000 },
"source": { "type": "deposit", "id": "dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B" },
"reverses_balance_transaction_id": "btxn_01K3M9ZZ4P7QW2NHT0RXKD5V8C",
"reason_code": "SLIP_INVALIDATED_AFTER_SETTLEMENT",
"reason_display_th": "รายการถูกยกเลิกหลังจากตรวจพบว่าสลิปไม่ถูกต้อง",
"created_at": "2026-08-21T09:41:19.884Z"
}ยอดคงเหลือ 3 ถัง (GET /v1/pay/balance)
| ฟิลด์ | ชนิด | หน่วย | ความหมาย |
|---|---|---|---|
available_minor | integer | สตางค์ | 🔴 ยอดที่สร้างรายการถอนได้จริง — คำนวณเงินถอนจากตัวนี้เท่านั้น |
pending_clearing_minor | integer | สตางค์ | ยอดที่เครดิตแล้วแต่ยังไม่ครบ 72 ชั่วโมง ⇒ ยังถอนไม่ได้ |
reserved_minor | integer | สตางค์ | ยอดที่ถูกล็อกไว้กับรายการถอนที่เปิดค้างอยู่ |
currency | string | — | "THB" |
as_of | string RFC3339 UTC | — | ยอดนี้เป็นของเวลาไหน |
แสดงให้ผู้ใช้เห็น ใช้ available_minor + pending_clearing_minor · คำนวณว่าถอนได้เท่าไหร่ ใช้ available_minor อย่างเดียว
ฟิลด์ที่คุณส่งมาให้เรา
POST /v1/pay/deposits
| ฟิลด์ | ชนิด | บังคับ | หมายเหตุ |
|---|---|---|---|
amount_minor | integer สตางค์ | ✅ | 🔴 ยอดกลม ไม่มีเศษสตางค์ — เราจับคู่ด้วยตัวตนผู้โอน ไม่ใช่ด้วยเศษสตางค์ |
currency | string | ✅ | "THB" |
merchant_reference | string ≤ 64 | ✅ | ไม่ซ้ำต่อ merchant |
end_user_ref | string ≤ 64 | ✅ | id ผู้ใช้ในระบบคุณ · ใช้กับ GET /v1/pay/senders ด้วย |
sender{} หรือ sender_id | object / string | ✅ | 🔴 ต้องมีอย่างใดอย่างหนึ่ง ไม่มี = 422 sender_required |
sender.bank_code | string 3 หลัก | ✅ (ถ้าใช้ sender{}) | จาก GET /v1/pay/banks |
sender.account_number | string ตัวเลขล้วน | ✅ (ถ้าใช้ sender{}) | ตัด - และช่องว่างออกก่อนส่ง |
sender.account_name_th | string | ✅ (ถ้าใช้ sender{}) | 🔴 ห้ามมีคำนำหน้า (นาย/นาง/น.ส./คุณ) |
sender.account_name_en | string | optional | ช่วยให้ผ่านการตรวจอัตโนมัติมากขึ้น |
client.device_fingerprint | string | optional จริง ๆ แต่ต้องส่ง | ไม่ส่ง = เพดานรายการเปิดพร้อมกันเหลือ 1 และตกไป bucket rate limit รวม |
client.ip | string | เหมือนบน | |
client.user_agent | string | เหมือนบน | |
fill_preference | "any" | "internal_only" | optional | ค่าเริ่มต้น "any" |
min_ttl_seconds | integer (วินาที) | optional | 0–86,400; ถ้าไม่พอดีกับอายุรายการหลัง clamp จะได้ 422 |
expires_in_seconds | integer (วินาที) | optional | 60–86,400 และถูกจำกัดด้วยนโยบายของแบรนด์อีกชั้น |
return_url | string URL | optional | |
metadata | object ≤ 20 key | optional | ค่าละ ≤ 500 ไบต์ |
POST /v1/pay/withdrawals
| ฟิลด์ | ชนิด | บังคับ | หมายเหตุ |
|---|---|---|---|
amount_minor | integer สตางค์ | ✅ | ยอดถอน (ยังไม่รวมค่าธรรมเนียม) |
currency | string | ✅ | "THB" |
merchant_reference | string ≤ 64 | ✅ | |
end_user_ref | string ≤ 64 | ✅ | |
beneficiary_id หรือ beneficiary{} | string / object | ✅ | 🔴 บัญชีต้อง verified แล้ว ไม่งั้น 409 beneficiary_not_verified |
beneficiary.bank_code / account_number / account_name_th / promptpay_id | string | ✅ (ถ้าใช้ beneficiary{}) | promptpay_id เป็น optional |
client{} | object | เหมือนฝั่งฝาก | |
metadata | object | optional |
Header ของทุก request
| Header | บังคับ | หมายเหตุ |
|---|---|---|
Authorization: Bearer sk_live_* | ✅ | เก็บใน secret manager หมุนได้โดยไม่ต้อง deploy |
PG-Timestamp | ✅ | unix seconds · ต่างจากเวลาจริงได้ ±300 วินาที |
PG-Nonce | ✅ | 🔴 ใหม่ทุก attempt ห้ามซ้ำภายใน 10 นาที |
PG-Version | ✅ | เวอร์ชัน API ที่คุณตรึงไว้ เช่น 2026-08-01 · 🔴 อยู่ใน canonical string ด้วย |
Idempotency-Key | ✅ เฉพาะ POST | 🔴 คงเดิมตลอด logical operation · อยู่ใน canonical string ด้วย |
PG-Signature | ✅ | v1= + HMAC-SHA256 hex ตัวพิมพ์เล็ก |
Header ของ webhook ที่เรายิงไปหาคุณ
| Header | หมายเหตุ |
|---|---|
X-PG-Signature | รูปแบบ v1,kid=<kid>,t=<unix_seconds>,s=<base64url ไม่มี padding> · 🔴 parse แบบ key=value ห้าม parse ด้วยตำแหน่ง |
X-PG-Event-Id | ต้องตรงกับ id ใน body |
X-PG-Event-Type | ชื่อ event |
X-PG-Delivery-Id | id ของการส่งครั้งนี้ |
X-PG-Endpoint-Id | endpoint ปลายทางของการส่งครั้งนี้ |
X-PG-Attempt | ครั้งที่เท่าไหร่ (1–11) |
X-PG-Resend | optional · ไม่มีใน delivery ปกติ · manual เมื่อกดส่งซ้ำ |
X-PG-Api-Version | เวอร์ชันที่ render payload |
X-PG-Livemode | true / false |
business event อยู่ใน body และ immutable ส่วน delivery metadata อยู่ใน header เท่านั้น Header กลุ่ม delivery ไม่อยู่ใน canonical signature จึงใช้เพื่อ log/trace หลัง verify เท่านั้น ห้ามใช้ตัดสินเรื่องเงิน สิทธิ์ หรือ state
ตารางคู่ที่สับสนกันบ่อย
| คู่ที่สับสน | ต่างกันยังไง | จำยังไง |
|---|---|---|
transferred_amount_minor vs amount_received_minor | settlement evidence V vs ยอดที่รับเข้า/เครดิต A | ต่างกันเมื่อ same-slip overpay; additional transfer อยู่ใน E แยกและไม่ทำให้ V สะสม |
amount_minor vs amount_received_minor | amount_minor = ยอดที่ สั่ง · amount_received_minor = ยอดที่ระบบรับเข้า | ต่างกันเมื่อ partial:true; overpay จะเท่ากันและมี flag |
amount_received_minor vs credited_amount_minor | ยอดของ ผู้ใช้ vs ยอดของ merchant | ผู้ใช้ได้ A · merchant ได้ A หลังหักค่าธรรมเนียม |
ordered_amount_minor vs amount_minor | ค่าเดียวกัน แต่ ordered_amount_minor โผล่มาใน deposit.settled เพื่อให้เทียบกับยอดธนาคารยืนยันและยอดรับเข้าได้ในใบเดียว | ใช้ transferred_amount_minor จำแนก short/exact/over แล้วเครดิต amount_received_minor |
fee_total_minor (ฝาก) vs fee_total_minor (ถอน) | ฝาก: หักออก จากยอดที่เข้ากระเป๋า · ถอน: บวกเพิ่ม จากยอดที่ต้องมี | ฝาก received − fee = credited · ถอน ต้องมี available ≥ amount + fee |
fee_total_minor vs fee_recognized_minor | ล็อกไว้ทั้งก้อนตอนสร้าง vs ถูกใช้จริงตามสัดส่วนที่จ่ายออกแล้ว | ใบที่หมดอายุกลางทางจะคืนส่วนต่างของสองค่านี้ |
filled_minor vs amount_total_minor | จ่ายไปแล้ว vs สั่งไว้ทั้งหมด | ปิดใบได้เมื่อเท่ากัน และ remaining_minor = 0 |
remaining_minor vs reserved_minor | ยังไม่มีใครจอง vs มีคนกำลังโอนอยู่ | total = filled + reserved + remaining เสมอ |
refunded_minor vs amount_total_minor | ยอดที่คืนจริง vs ยอดที่สั่ง | 🔴 ใบ expired ที่เคยจ่ายไปบางส่วน คืนแค่ refunded_minor |
available_minor vs pending_clearing_minor | ถอนได้ vs ยังถอนไม่ได้จนครบ 72 ชั่วโมง | แสดงผลใช้ผลรวม · คำนวณการถอนใช้ available_minor |
expires_at (รายการ) vs legs[].expires_at | หมดเวลาทั้งรายการ vs หมดเวลาของก้อนนั้น | นับถอยหลังบนหน้าจอใช้ของรายการ |
late_slip_accepted_until vs expires_at | หน้าต่างอัปสลิปย้อนหลัง vs เวลาที่ต้องโอนให้เสร็จ | late_slip_accepted_until ยาวกว่าเสมอ (อีก 24 ชั่วโมง) |
is_terminal vs status | จบจริงหรือยัง vs ชื่อสถานะ | 🔴 เช็ค is_terminal ไม่ใช่ชื่อสถานะ |
leg_ref vs deposit_id / id ของใบถอน | id ของก้อนย่อย vs id ของใบ | leg_ref ของสองฝั่ง ไม่เท่ากัน โดยตั้งใจ |
event.type vs data.object.status | มีอะไรเกิดขึ้น vs ความจริงตอนนี้ | 🔴 ตัดสินใจจาก data.object.status |
event.id vs X-PG-Delivery-Id | id ของเหตุการณ์ใน body (คงเดิมทุก attempt) vs id ของการส่งใน header (เปลี่ยนทุกครั้ง) | กันซ้ำด้วย event.id เท่านั้น |
sequence vs version | ลำดับ event ทั้งบัญชี vs ลำดับการเปลี่ยนแปลงของ object นั้น | ตรวจ event หายใช้ sequence · กัน event เก่าทับใหม่ใช้ version |
merchant_reference vs id | เลขของคุณ vs เลขของเรา | 🔴 เปิด ticket และกระทบยอดใช้ id ของเรา |
balance.credited vs balance.cleared | เงินเข้าใหม่ vs ย้ายถังจาก pending_clearing ไป available | 🔴 balance.cleared ห้ามบวกยอดซ้ำ |
ชื่อที่เลิกใช้แล้ว
ถ้าเจอชื่อฝั่งซ้ายในเอกสารเก่าหรือในโค้ดของคุณ ให้เปลี่ยนเป็นฝั่งขวา
| เลิกใช้ | ชื่อที่ถูก |
|---|---|
amount_received · fee_total · credited_amount | amount_received_minor · fee_total_minor · credited_amount_minor |
payment_instructions[] · bank_account · promptpay | legs[] |
allocation_id | leg_ref |
source | liquidity_source |
late_slip_until | late_slip_accepted_until |
cancel_reason | cancel_reason_code |
reversal_of · ledger_txn_id | reversal.reverses_balance_transaction_id และ reversal.balance_transaction_id |
reference · order_ref | merchant_reference (ชื่อนี้ใช้ได้เฉพาะฝั่งคุณ) |
decision_source | settled_via |
เวลาแบบมี offset +07:00 | RFC3339 UTC ลงท้าย Z มิลลิวินาที 3 หลัก |
ขั้นต่อไป
| ถ้าคุณกำลัง… | ไปที่ |
|---|---|
| ออกแบบตารางฝั่งคุณ | State machine เพื่อดูว่าต้องมีคอลัมน์อะไรบ้าง |
| เขียน handler ของ webhook | ภาพรวมและซองข้อมูล · รายการ event ทั้งหมด |
| เจอค่ายอดถอนแสดงผิด | ลำดับไม่การันตี — version guard |
อยากรู้ว่า error แต่ละตัวมี details อะไร | Error code ทั้งหมด |
| ย้ายมาจาก gateway เจ้าอื่น | ย้ายจากผู้ให้บริการเดิม |
| กำลังจะขึ้นจริง | เช็กลิสต์ก่อน go-live |
