สร้างรายการฝาก
POST /v1/pay/deposits คือจุดเริ่มของทุกรายการเติมเงิน ปลายทางที่ผู้ใช้จะโอนไม่ใช่บัญชีของ gateway แต่เป็นบัญชีของผู้ถอนอีกราย ระบบจึงต้องรู้ตั้งแต่ตอนสร้างว่า ใครจะเป็นคนกดโอน ไม่ใช่รู้ทีหลังตอนสลิปเข้ามา
endpoint นี้ตอบได้ 2 แบบที่ถือว่าสำเร็จทั้งคู่
| HTTP | status | แปลว่า |
|---|---|---|
201 | awaiting_payment | จับคู่ได้แล้ว มี legs[] ให้โอน |
202 | awaiting_liquidity | สร้างรายการแล้ว แต่ยังไม่มีปลายทางให้โอน ต้องรอ |
อ่านตรงนี้ก่อนเขียนโค้ด
202 ไม่ใช่ error และเป็นเส้นที่ gateway เจ้าอื่นในตลาดไม่มี โค้ดที่เขียน if (res.status !== 200) throw หรือ if (!res.ok) retry จะพังทั้งเส้นนี้ — ผู้ใช้เห็นข้อความว่าเติมเงินไม่สำเร็จ ทั้งที่รายการถูกสร้างเรียบร้อยแล้วและกำลังจะได้ QR ในไม่กี่วินาที · รายละเอียดที่ หัวข้อ 202
ก่อนยิง ต้องทำ 3 อย่าง
| # | ทำอะไร | ทำไม |
|---|---|---|
| 1 | เรียก GET /v1/pay/senders แล้วให้ผู้ใช้ เลือก บัญชีผู้โอน | ผู้ใช้ที่เลือกจากลิสต์แทบไม่เข้าคิวเจ้าหน้าที่เลย · ผู้ใช้ที่พิมพ์เองเข้าคิวสูง |
| 2 | เทาปุ่มธนาคารตาม GET /v1/pay/banks | ธนาคารที่ปิดรับจะได้ 409 ที่ขั้นสุดท้าย = ผู้ใช้ทิ้งรายการกลางคัน |
| 3 | เขียน merchant_reference + Idempotency-Key ลง DB ของคุณ ก่อน ยิง request | retry ต้องอ่าน key เดิมจาก DB ถ้า gen ใหม่ทุกครั้งจะได้ order ซ้ำและ QR สองใบ |
Request
Header ที่บังคับ
Authorization: Bearer sk_live_xxx
PG-Timestamp: 1755749553
PG-Nonce: 01JQA5T8V0X2Z4B6D8F0H2J4L6
PG-Version: 2026-08-01
Idempotency-Key: topup-ORD-9981-01
PG-Signature: v1=<hex lowercase ของ HMAC-SHA256>
Content-Type: application/jsonIdempotency-Key และ PG-Version อยู่ใน canonical string ของลายเซ็น ลืมใส่ = 401 ไม่ใช่ 400 · วิธีเซ็นดูที่ การยืนยันตัวตนของ request
Body
| field | ชนิด | บังคับ | ข้อจำกัด |
|---|---|---|---|
amount_minor | integer | ✅ | > 0 · หน่วยสตางค์ · 🔴 ยอดกลม ไม่มีเศษสตางค์ (5,000.00 บาท = 500000) · นอกช่วง policy หรือมีเศษ → 422 amount_not_supported |
currency | string | ✅ | ปัจจุบันรองรับ "THB" เท่านั้น |
merchant_reference | string | ✅ | unique ต่อ merchant · ≤ 64 ตัวอักษร |
end_user_ref | string | ✅ | id ของผู้ใช้ในระบบคุณ · ≤ 64 ตัวอักษร · ใช้กับ GET /v1/pay/senders ด้วย |
sender | object | ✅ (หรือ sender_id) | บัญชีที่ผู้ใช้จะกดโอนจริง · ดู โครงสร้างด้านล่าง |
sender_id | string | ✅ (หรือ sender) | id จาก GET /v1/pay/senders · 🔴 แบบนี้การันตีว่าไม่เข้าคิวเพราะชื่อ/เลขบัญชีไม่ตรง |
client | object | ⚠️ | บังคับโดยพฤตินัย — ไม่ส่งแล้วเพดานและ rate limit เปลี่ยน ดูตารางผลของการไม่ส่ง |
fill_preference | string | — | "any" (default) หรือ "internal_only" |
min_ttl_seconds | integer | — | 0–86,400 · ถ้าเวลาที่เหลือสั้นกว่านี้จะข้าม route; ถ้าค่าไม่พอดีกับอายุรายการหลัง clamp จะได้ 422 |
expires_in_seconds | integer | — | 60–86,400 และ clamp ด้วย policy อีกชั้น · 🔴 ค่าที่ใช้จริงคือ expires_at ใน response เท่านั้น |
return_url | string | — | URL กลับหลังผู้ใช้จ่ายเสร็จบนหน้า checkout_url |
metadata | object | — | ≤ 20 keys · ≤ 500 bytes ต่อค่า · ถูกส่งกลับมาในทุก webhook ของรายการนี้ |
sender แบบ A (กรอกเอง)
| field | ชนิด | บังคับ | ข้อจำกัด |
|---|---|---|---|
sender.bank_code | string | ✅ | 3 หลัก จาก GET /v1/pay/banks |
sender.account_number | string | ✅ | ตัวเลขล้วน — client ต้องตัด - และช่องว่างออกก่อนส่ง |
sender.account_name_th | string | ✅ | 🔴 ห้ามมีคำนำหน้า (นาย นาง น.ส. คุณ) เพราะธนาคารไม่ส่งคำนำหน้ามาในสลิป |
sender.account_name_en | string | — | ไม่บังคับ แต่ช่วยให้ผ่านการตรวจอัตโนมัติมากขึ้น |
client — ไม่ส่งแล้วเจ็บ
ส่ง client{} | เพดานรายการค้างต่อผู้ใช้ | bucket ของ rate limit |
|---|---|---|
ครบ (device_fingerprint + ip) | นับแยกราย (merchant, end_user_ref, device_fingerprint) — เพดาน 2 | pay_create_per_user 10 ครั้ง/นาที ต่อผู้ใช้ |
| ไม่ส่ง | 🔴 นับแค่ (merchant, end_user_ref) ซึ่งปลอมได้ฟรี ⇒ เราลดเพดานเหลือ 1 | 🔴 ตกไป bucket หยาบที่แชร์กับผู้ใช้ทุกคนของคุณ ⇒ merchant ขนาดกลางชน 429 ภายในนาทีแรก |
⇒ ปฏิบัติจริง: ส่ง client{} ทุกครั้ง
ตัวอย่าง request
curl -X POST https://api.example-wl.com/v1/pay/deposits \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Timestamp: 1755749553" \
-H "PG-Nonce: 01JQA5T8V0X2Z4B6D8F0H2J4L6" \
-H "PG-Version: 2026-08-01" \
-H "Idempotency-Key: topup-ORD-9981-01" \
-H "PG-Signature: v1=9c2f...b41e" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": 500000,
"currency": "THB",
"merchant_reference": "ORD-9981",
"end_user_ref": "u_88213",
"sender": {
"bank_code": "004",
"account_number": "1234567890",
"account_name_th": "สมชาย ใจดี",
"account_name_en": "SOMCHAI JAIDEE"
},
"client": {
"device_fingerprint": "df_9a1c7f2b0e",
"ip": "203.0.113.44",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)"
},
"metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
}'ถ้าผู้ใช้เคยเติมเงินมาแล้ว ให้ส่ง sender_id แทนทั้ง object
{
"amount_minor": 500000,
"currency": "THB",
"merchant_reference": "ORD-9982",
"end_user_ref": "u_88213",
"sender_id": "snd_01K2QF7V8H3NPD0YXW4RJ6ZB5C",
"client": {
"device_fingerprint": "df_9a1c7f2b0e",
"ip": "203.0.113.44",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)"
}
}Response 201 — จับคู่ได้ทันที
{
"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",
"checkout_url": "https://pay.example-wl.com/c/9f2c1ab6e0d74f",
"slip_upload": {
"token": "pgsut_eyJraWQiOiJvcmRlc...",
"expires_at": "2026-08-22T04:30:47.902Z",
"upload_url": "https://slip-api.example.com/v1/pay/deposits/dep_01K3M7Y9RS5TQ0WH2E7XKJ4C6B/slip",
"max_bytes": 4194304,
"content_types": ["image/jpeg", "image/png", "image/webp", "application/json"]
},
"liquidity_source": "internal",
"leg_count": 1,
"legs": [
{
"leg_ref": "leg_7QF2H8KM",
"sequence": 1,
"amount_minor": 500000,
"expires_at": "2026-08-21T04:30:47.902Z",
"leg_status": "pending",
"method": "promptpay_qr",
"qr_payload": "00020101021229370016A00000067701011201150105560012345530376454065000.005802TH6304A1B2",
"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": "ส*** ใ***"
}
}
],
"sender": {
"bank_code": "004",
"account_number_masked": "xxx-x-x7890",
"account_name_th_masked": "ส*** ใ***"
},
"metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
}สิ่งที่ต้องทำกับ 201
- แสดง QR หรือเลขบัญชีจาก
legs[] - เก็บ
slip_uploadไว้เฉพาะใน secure checkout session อายุสั้น: ส่งไปที่upload_urlตามที่คืนมาเท่านั้น (ห้ามประกอบ host/path เอง), ใช้tokenเป็น bearer และ precheck ไฟล์ด้วยmax_bytes+content_types - นับถอยหลังจาก
expires_atที่ได้มา 🔴 ห้าม hardcode 15 นาที บางเส้นทางหมดอายุจริงใน ~4 นาที - แสดงกล่อง "โอนจากบัญชีนี้เท่านั้น" ให้เด่นกว่าตัว QR (sender-account)
- ขึ้นข้อความ "QR นี้ใช้ได้ครั้งเดียวสำหรับรายการนี้ ห้ามบันทึกภาพไว้ใช้ครั้งหน้า"
legs[] มีได้มากกว่า 1 รายการ
legs.length > 1 แปลว่าผู้ใช้ต้องโอน หลายครั้ง ไม่ใช่เลือกโอนอันใดอันหนึ่ง · legs[0] ไม่ใช่ทั้งหมดของยอด · UI ต้องรองรับ "ขั้นที่ 1 จาก 2" และแสดง amount_minor ของแต่ละ leg แยกกัน · อ่าน เลือกช่องทางและตัวเลือกของผู้ใช้
ห้ามเก็บ ห้าม log
qr_payload, ข้อมูลบัญชีปลายทางแบบเต็ม และ slip_upload.token ห้ามเขียนลง DB ถาวร ห้ามลง log ห้ามใส่ URL/analytics ห้ามส่งต่อ · token เก็บได้เฉพาะ secure checkout session อายุสั้นจนหมดอายุ ส่วนข้อมูลจ่ายเงินใช้แสดงผลแล้วทิ้ง · ทางที่ปลอดภัยกว่าเสมอคือส่งผู้ใช้ไปที่ checkout_url
Response 202 — ยังไม่มีสภาพคล่อง
นี่คือจุดที่ต่างจาก gateway เจ้าอื่นชัดที่สุด และเป็นจุดที่ merchant เขียนโค้ดพลาดบ่อยที่สุด
ที่อื่นทำแบบไหน — ตอนไม่มีบัญชีปลายทางให้โอน ระบบจะตอบ error (503, no_channel_available) แล้วให้ merchant ลองใหม่เอง รายการไม่ถูกสร้าง
เราทำแบบไหน — เราสร้างรายการเสมอ แล้วตอบ 202 พร้อมสถานะ awaiting_liquidity รายการมี id มี expires_at มี event stream ครบตั้งแต่วินาทีนั้น ระบบจะไล่หาปลายทางให้เองทุก ~10 วินาที แล้วยิง webhook deposit.matched เมื่อได้
ทำไมถึงออกแบบแบบนี้ — ถ้าตอบ error แล้วไม่สร้างรายการ ผู้ใช้ที่กดเติมเงินตอนสภาพคล่องบางจะเห็นหน้าจอ error ทั้งที่อีก 15 วินาทีก็มีปลายทางแล้ว · การมี id ตั้งแต่วินาทีแรกยังทำให้ support ตามรายการได้ และทำให้ Idempotency-Key ของคุณผูกกับรายการจริงไม่ใช่กับความว่างเปล่า
{
"id": "dep_01K3M8B2VQ7HYD4NKR0XF9TW6E",
"object": "deposit_order",
"version": 1,
"status": "awaiting_liquidity",
"is_terminal": false,
"merchant_reference": "ORD-9983",
"amount_minor": 500000,
"currency": "THB",
"created_at": "2026-08-21T04:10:47.902Z",
"expires_at": "2026-08-21T04:30:47.902Z",
"legs": [],
"liquidity": {
"wait_started_at": "2026-08-21T04:10:47.902Z",
"estimated_wait_ms": 25000,
"retry_interval_ms": 10000,
"suggested_amounts_minor": [300000, 400000, 1000000]
},
"retry_after_seconds": 10,
"sender": {
"bank_code": "004",
"account_number_masked": "xxx-x-x7890",
"account_name_th_masked": "ส*** ใ***"
},
"metadata": { "order_id": "ORD-9983" }
}ต้องทำอะไรกับ 202
| ✅ ทำ | ❌ ห้ามทำ |
|---|---|
เก็บ id ลง DB เหมือน 201 ทุกประการ | ❌ ทิ้ง response แล้วบอกผู้ใช้ว่าล้มเหลว |
| แสดงหน้า "กำลังจัดหาช่องทางโอน กรุณารอสักครู่" + spinner | ❌ แสดง QR — legs[] ว่างเปล่า ยังไม่มีปลายทางจริง ๆ |
เสนอยอดจาก liquidity.suggested_amounts_minor[] ให้ผู้ใช้เลือก ถ้าเขารีบ | ❌ ยิง POST /v1/pay/deposits ซ้ำ — idempotency จะคืนรายการเดิมอยู่ดี ส่วนถ้าเปลี่ยน key จะได้รายการซ้ำสองใบ |
รอ webhook deposit.matched แล้วให้ backend เรียก POST /v1/pay/deposits/{id}/slip-token ด้วย API key + HMAC; ได้ capability แล้วจึงแสดง QR | ❌ poll ทุกวินาที — ใช้ retry_after_seconds เป็นจังหวะขั้นต่ำ |
ตั้ง countdown จาก expires_at ที่ได้มาแล้ว และอ่านค่าใหม่ตอน deposit.matched | ❌ ถือว่า expires_at ที่ได้ตอน 202 เป็นค่าสุดท้าย |
รายการที่ awaiting_liquidity จะจบยังไง
| ปลายทาง | เกิดเมื่อ | webhook |
|---|---|---|
awaiting_payment | หาปลายทางได้ | deposit.matched — event เดียวที่มี QR อ่าน legs[] และ expires_at ใหม่ทั้งหมด |
canceled | หมดเวลารอแล้วยังไม่มีปลายทาง · cancel_reason_code: "no_liquidity_available" | deposit.canceled |
ไม่มีสภาพคล่องแล้วปิดรายการ จะเป็น canceled ไม่ใช่ expired
เพราะ expired เปิดหน้าต่างอัปสลิปย้อนหลัง 24 ชั่วโมง แต่รายการนี้ไม่เคยมีปลายทางให้โอน ⇒ ปุ่ม "ฉันโอนไปแล้ว" จะสร้างได้แค่ ticket เปล่า · deposit.canceled เส้นนี้จะมี late_slip_accepted_until เป็น null และ is_terminal: true
deposit.created มาทั้งสองเส้น
ทั้ง 201 และ 202 ยิง deposit.created โดย data.object.status เป็นค่าจริงของเส้นนั้น (awaiting_payment หรือ awaiting_liquidity) ⇒ handler ของคุณต้องไม่ assume ว่า deposit.created แปลว่ามี QR แล้ว
slip_upload.token เป็น bearer secret จึง ไม่อยู่ใน webhook ใด ๆ เมื่อเส้น 202 เปลี่ยนเป็น awaiting_payment ให้ใช้ deposit_id จาก deposit.matched ไป mint ผ่าน /slip-token จาก backend ก่อนเปิด payment instructions
กับดัก idempotency ของเส้น 202
ยิง POST ซ้ำด้วย Idempotency-Key เดิม + body เดิม จะได้ response เดิมทั้งดุ้น รวม HTTP status เดิม พร้อม header Idempotent-Replay: true
⇒ ถ้าครั้งแรกได้ 202 แล้วอีก 30 วินาทีคุณยิงซ้ำ คุณจะได้ 202 กลับมาอีก ทั้งที่ตอนนี้รายการเดินไปถึง awaiting_payment แล้ว · สถานะจริงอ่านได้จาก GET /v1/pay/deposits/{id} หรือจาก webhook เท่านั้น ห้ามอ่านจาก replay body
โค้ดที่รับมือทั้ง 201 และ 202 ได้
Node (fetch)
// sign() คือฟังก์ชันเซ็น HMAC ของคุณเอง ดู /get-started/authentication
async function createDeposit(order) {
const body = JSON.stringify({
amount_minor: order.amountMinor,
currency: 'THB',
merchant_reference: order.ref,
end_user_ref: order.userRef,
sender_id: order.senderId,
client: {
device_fingerprint: order.deviceFp,
ip: order.ip,
user_agent: order.userAgent,
},
metadata: { order_id: order.ref },
})
// 🔴 key ต้องถูกเขียนลง DB ก่อนถึงบรรทัดนี้ และ retry ต้องอ่านค่าเดิมกลับมา
const res = await fetch('https://api.example-wl.com/v1/pay/deposits', {
method: 'POST',
headers: sign('POST', '/v1/pay/deposits', '', body, order.idempotencyKey),
body,
})
const data = await res.json()
// ✅ 2xx = สำเร็จทั้งหมด ห้ามเช็ค === 200
if (res.status === 201) {
return { kind: 'ready', deposit: data } // แสดง legs[] + countdown
}
if (res.status === 202) {
return { kind: 'waiting', deposit: data } // แสดงหน้ารอ + suggested_amounts_minor
}
// ทุกอย่างที่เหลือคือ error จริง — แยกตาม code ไม่ใช่ตาม HTTP อย่างเดียว
throw Object.assign(new Error(data.error.message), {
code: data.error.code,
retryable: data.error.retryable,
retryAfter: data.error.retry_after_seconds,
details: data.error.details,
requestId: data.error.request_id,
})
}PHP
<?php
// $sign() คือฟังก์ชันเซ็น HMAC ของคุณเอง ดู /get-started/authentication
function createDeposit(array $order, array $headers): array
{
$body = json_encode([
'amount_minor' => $order['amount_minor'],
'currency' => 'THB',
'merchant_reference' => $order['ref'],
'end_user_ref' => $order['user_ref'],
'sender_id' => $order['sender_id'],
'client' => [
'device_fingerprint' => $order['device_fp'],
'ip' => $order['ip'],
'user_agent' => $order['user_agent'],
],
'metadata' => ['order_id' => $order['ref']],
], JSON_UNESCAPED_UNICODE);
$ch = curl_init('https://api.example-wl.com/v1/pay/deposits');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers, // รวม Idempotency-Key + PG-Signature แล้ว
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true);
// ✅ 201 และ 202 สำเร็จทั้งคู่
if ($status === 201) {
return ['kind' => 'ready', 'deposit' => $data];
}
if ($status === 202) {
return ['kind' => 'waiting', 'deposit' => $data];
}
throw new RuntimeException(
$data['error']['code'] . ': ' . $data['error']['message_th'],
$status
);
}Error ทุกแบบของ endpoint นี้
รูปแบบ error body เหมือนกันทุก endpoint
{
"error": {
"code": "sender_bank_unavailable",
"type": "invalid_request_error",
"message": "The sending bank is not accepting new deposits right now.",
"message_th": "ธนาคารต้นทางปิดรับรายการใหม่ชั่วคราว",
"retryable": true,
"retry_after_seconds": null,
"request_id": "req_01JQA5T8V0X2Z4B6D8F0H2J4L6",
"details": {
"bank_code": "004",
"retry_after_hint_seconds": 1800,
"public_message_th": "ธนาคารกสิกรไทยปิดปรับปรุงระบบชั่วคราว กรุณาเลือกธนาคารอื่น",
"alternative_banks": ["002", "014", "006"]
}
}
}| HTTP | code | เกิดเมื่อ | retry ได้ | ต้องทำอะไร |
|---|---|---|---|---|
422 | sender_required | ไม่ส่งทั้ง sender และ sender_id | ❌ | เพิ่มหน้าจอเลือกบัญชีผู้โอน — อ่านที่นี่ |
422 | validation_error | field ผิดรูป | ❌ | ดู details.fields[] แล้วแก้ payload |
422 | amount_not_supported | ยอดต่ำ/สูงเกิน policy หรือมีเศษสตางค์ | ❌ | ปัดเป็นยอดกลม · ใช้ details.min_minor / details.max_minor |
409 | sender_bank_unavailable | ธนาคารของผู้โอนปิดรับรายการใหม่ | ✅ | ให้ผู้ใช้เลือกจาก details.alternative_banks[] · แสดง details.public_message_th · 🔴 สลิปของรายการเดิมยังส่งได้เสมอ การปิดธนาคารไม่กระทบรายการที่เปิดอยู่แล้ว |
409 | duplicate_open_deposit | มีรายการ awaiting_payment ยอดเท่ากัน + บัญชีผู้โอนเดียวกัน ค้างอยู่ | ❌ | ชี้ผู้ใช้กลับไปที่ details.open_deposit_ids[0] อย่าสร้างใบใหม่ |
409 | too_many_open_deposits | เกินเพดานรายการค้างต่อผู้ใช้ (2 ใบ · 1 ใบถ้าไม่ส่ง client{}) | ❌ | ให้ผู้ใช้ปิดหรือยกเลิกรายการเดิมก่อน — details.open_deposit_ids[] · ดูวิธีจัดการรายการค้าง |
400 | idempotency_key_required | ไม่ส่ง Idempotency-Key | ❌ | เพิ่ม header (และเขียนลง DB ก่อนยิง) |
409 | idempotency_key_reused | key เดิม แต่ body ต่าง | ❌ | เปลี่ยน body เมื่อไหร่ ต้องเปลี่ยน key ด้วย · details.original_request_id บอกใบเดิม |
409 | idempotency_in_flight | key เดิม แต่ request แรกยังไม่จบ (double-click) | ✅ | รอ retry_after_seconds (= 1) แล้วยิงซ้ำด้วย key เดิม |
401 | signature_invalid · timestamp_out_of_window · nonce_replayed | ลายเซ็น/นาฬิกา/nonce | บางตัว | ดู การยืนยันตัวตนของ request · PG-Nonce ต้องใหม่ทุก attempt แต่ Idempotency-Key ต้องเดิม |
403 | tenant_suspended · scope_denied | บัญชีถูกระงับ / key ไม่มีสิทธิ์ | ❌ | ติดต่อผู้ให้บริการของคุณ |
429 | rate_limited | ชน bucket pay_write (600/นาที ต่อ key) หรือ pay_create_per_user (10/นาที ต่อผู้ใช้) | ✅ | เคารพ Retry-After + jitter ±20% · 🔴 ห้าม retry เป็นลูปติด ๆ มี penalty ทวีคูณ |
503 | route_unavailable | ระบบกำลังลดโหลด | ✅ | แสดง "ระบบหนาแน่น ลองใหม่อีกครั้ง" + Retry-After · 🔴 ไม่ใช่บั๊ก ไม่ใช่ 500 |
503 | service_unavailable | ระบบลดโหลดทั้งระบบ | ✅ | เหมือนบน |
500 | internal_error | ฝั่งเราพัง | ✅ | ส่ง request_id ให้ support |
429 กับ 503 คนละเรื่อง
429 = คุณยิงเกินโควตาของตัวเอง แก้ที่การจัดจังหวะฝั่งคุณ · 503 = ระบบกำลังลดโหลด แก้ด้วยการรอตาม Retry-After · เมื่อระบบตึง การสร้างรายการฝากใหม่คือสิ่งแรกที่ถูกลดโหลด เพราะยังไม่มีใครโอนเงิน ⇒ ไม่มีใครเสียหาย ส่วนการอัปสลิปจะไม่ถูกลดโหลดเลย
ต้องเก็บอะไรลง DB
| ตอนไหน | เก็บอะไร |
|---|---|
| ก่อนยิง | merchant_reference (UNIQUE) · Idempotency-Key (UNIQUE) |
หลังได้ 201/202 | deposit_id (ใช้เป็น PK ฝั่งคุณ) · version · status · expires_at · snapshot ของ sender.* แบบ masked |
| หน้าโอน | เก็บแค่ expires_at · 🔴 qr_payload และเลขบัญชีปลายทางเต็ม ห้ามเก็บ ห้าม log |
คอลัมน์ deposit_id ต้องเป็น text
ถ้าคุณย้ายมาจาก gateway เจ้าอื่น อย่าใช้ varchar(20) และอย่า parse prefix — รูปแบบ id ของเราต่างจากเดิมและอาจเปลี่ยนความยาวได้
ขั้นต่อไป
- ทำไมต้องแจ้งบัญชีผู้โอน — หน้าที่ลดคิวเจ้าหน้าที่ได้มากที่สุด อ่านก่อนออกแบบหน้าเติมเงิน
- ธนาคารที่เปิดรับ — เอา
availableไปเทาปุ่มก่อนถึงขั้น409 - อัปโหลดสลิป — ขั้นถัดไปหลังผู้ใช้โอนแล้ว และเหตุผลที่ durable intake สำเร็จตอบ
202 - สถานะรายการฝาก — สถานะไหนแปลว่าเครดิตได้ และควร poll ถี่แค่ไหน
- ยกเลิกรายการฝาก — ยกเลิกได้เมื่อไร และทำไม
canceledไม่ใช่จุดจบเสมอไป - ลำดับ webhook ไม่การันตี — version guard ที่ต้องมีก่อนรับ event จริง
