Skip to content

สร้างรายการฝาก ​

POST /v1/pay/deposits คือจุดเริ่มของทุกรายการเติมเงิน ปลายทางที่ผู้ใช้จะโอนไม่ใช่บัญชีของ gateway แต่เป็นบัญชีของผู้ถอนอีกราย ระบบจึงต้องรู้ตั้งแต่ตอนสร้างว่า ใครจะเป็นคนกดโอน ไม่ใช่รู้ทีหลังตอนสลิปเข้ามา

endpoint นี้ตอบได้ 2 แบบที่ถือว่าสำเร็จทั้งคู่

HTTPstatusแปลว่า
201awaiting_paymentจับคู่ได้แล้ว มี legs[] ให้โอน
202awaiting_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 ของคุณ ก่อน ยิง requestretry ต้องอ่าน 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/json

Idempotency-Key และ PG-Version อยู่ใน canonical string ของลายเซ็น ลืมใส่ = 401 ไม่ใช่ 400 · วิธีเซ็นดูที่ การยืนยันตัวตนของ request

Body ​

fieldชนิดบังคับข้อจำกัด
amount_minorinteger✅> 0 · หน่วยสตางค์ · 🔴 ยอดกลม ไม่มีเศษสตางค์ (5,000.00 บาท = 500000) · นอกช่วง policy หรือมีเศษ → 422 amount_not_supported
currencystring✅ปัจจุบันรองรับ "THB" เท่านั้น
merchant_referencestring✅unique ต่อ merchant · ≤ 64 ตัวอักษร
end_user_refstring✅id ของผู้ใช้ในระบบคุณ · ≤ 64 ตัวอักษร · ใช้กับ GET /v1/pay/senders ด้วย
senderobject✅ (หรือ sender_id)บัญชีที่ผู้ใช้จะกดโอนจริง · ดู โครงสร้างด้านล่าง
sender_idstring✅ (หรือ sender)id จาก GET /v1/pay/senders · 🔴 แบบนี้การันตีว่าไม่เข้าคิวเพราะชื่อ/เลขบัญชีไม่ตรง
clientobject⚠️บังคับโดยพฤตินัย — ไม่ส่งแล้วเพดานและ rate limit เปลี่ยน ดูตารางผลของการไม่ส่ง
fill_preferencestring—"any" (default) หรือ "internal_only"
min_ttl_secondsinteger—0–86,400 · ถ้าเวลาที่เหลือสั้นกว่านี้จะข้าม route; ถ้าค่าไม่พอดีกับอายุรายการหลัง clamp จะได้ 422
expires_in_secondsinteger—60–86,400 และ clamp ด้วย policy อีกชั้น · 🔴 ค่าที่ใช้จริงคือ expires_at ใน response เท่านั้น
return_urlstring—URL กลับหลังผู้ใช้จ่ายเสร็จบนหน้า checkout_url
metadataobject—≤ 20 keys · ≤ 500 bytes ต่อค่า · ถูกส่งกลับมาในทุก webhook ของรายการนี้

sender แบบ A (กรอกเอง) ​

fieldชนิดบังคับข้อจำกัด
sender.bank_codestring✅3 หลัก จาก GET /v1/pay/banks
sender.account_numberstring✅ตัวเลขล้วน — client ต้องตัด - และช่องว่างออกก่อนส่ง
sender.account_name_thstring✅🔴 ห้ามมีคำนำหน้า (นาย นาง น.ส. คุณ) เพราะธนาคารไม่ส่งคำนำหน้ามาในสลิป
sender.account_name_enstring—ไม่บังคับ แต่ช่วยให้ผ่านการตรวจอัตโนมัติมากขึ้น

client — ไม่ส่งแล้วเจ็บ ​

ส่ง client{}เพดานรายการค้างต่อผู้ใช้bucket ของ rate limit
ครบ (device_fingerprint + ip)นับแยกราย (merchant, end_user_ref, device_fingerprint) — เพดาน 2pay_create_per_user 10 ครั้ง/นาที ต่อผู้ใช้
ไม่ส่ง🔴 นับแค่ (merchant, end_user_ref) ซึ่งปลอมได้ฟรี ⇒ เราลดเพดานเหลือ 1🔴 ตกไป bucket หยาบที่แชร์กับผู้ใช้ทุกคนของคุณ ⇒ merchant ขนาดกลางชน 429 ภายในนาทีแรก

⇒ ปฏิบัติจริง: ส่ง client{} ทุกครั้ง

ตัวอย่าง request ​

bash
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

json
{
  "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 — จับคู่ได้ทันที ​

json
{
  "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 ของคุณผูกกับรายการจริงไม่ใช่กับความว่างเปล่า

json
{
  "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) ​

js
// 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
<?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

json
{
  "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"]
    }
  }
}
HTTPcodeเกิดเมื่อretry ได้ต้องทำอะไร
422sender_requiredไม่ส่งทั้ง sender และ sender_id❌เพิ่มหน้าจอเลือกบัญชีผู้โอน — อ่านที่นี่
422validation_errorfield ผิดรูป❌ดู details.fields[] แล้วแก้ payload
422amount_not_supportedยอดต่ำ/สูงเกิน policy หรือมีเศษสตางค์❌ปัดเป็นยอดกลม · ใช้ details.min_minor / details.max_minor
409sender_bank_unavailableธนาคารของผู้โอนปิดรับรายการใหม่✅ให้ผู้ใช้เลือกจาก details.alternative_banks[] · แสดง details.public_message_th · 🔴 สลิปของรายการเดิมยังส่งได้เสมอ การปิดธนาคารไม่กระทบรายการที่เปิดอยู่แล้ว
409duplicate_open_depositมีรายการ awaiting_payment ยอดเท่ากัน + บัญชีผู้โอนเดียวกัน ค้างอยู่❌ชี้ผู้ใช้กลับไปที่ details.open_deposit_ids[0] อย่าสร้างใบใหม่
409too_many_open_depositsเกินเพดานรายการค้างต่อผู้ใช้ (2 ใบ · 1 ใบถ้าไม่ส่ง client{})❌ให้ผู้ใช้ปิดหรือยกเลิกรายการเดิมก่อน — details.open_deposit_ids[] · ดูวิธีจัดการรายการค้าง
400idempotency_key_requiredไม่ส่ง Idempotency-Key❌เพิ่ม header (และเขียนลง DB ก่อนยิง)
409idempotency_key_reusedkey เดิม แต่ body ต่าง❌เปลี่ยน body เมื่อไหร่ ต้องเปลี่ยน key ด้วย · details.original_request_id บอกใบเดิม
409idempotency_in_flightkey เดิม แต่ request แรกยังไม่จบ (double-click)✅รอ retry_after_seconds (= 1) แล้วยิงซ้ำด้วย key เดิม
401signature_invalid · timestamp_out_of_window · nonce_replayedลายเซ็น/นาฬิกา/nonceบางตัวดู การยืนยันตัวตนของ request · PG-Nonce ต้องใหม่ทุก attempt แต่ Idempotency-Key ต้องเดิม
403tenant_suspended · scope_deniedบัญชีถูกระงับ / key ไม่มีสิทธิ์❌ติดต่อผู้ให้บริการของคุณ
429rate_limitedชน bucket pay_write (600/นาที ต่อ key) หรือ pay_create_per_user (10/นาที ต่อผู้ใช้)✅เคารพ Retry-After + jitter ±20% · 🔴 ห้าม retry เป็นลูปติด ๆ มี penalty ทวีคูณ
503route_unavailableระบบกำลังลดโหลด✅แสดง "ระบบหนาแน่น ลองใหม่อีกครั้ง" + Retry-After · 🔴 ไม่ใช่บั๊ก ไม่ใช่ 500
503service_unavailableระบบลดโหลดทั้งระบบ✅เหมือนบน
500internal_errorฝั่งเราพัง✅ส่ง request_id ให้ support

429 กับ 503 คนละเรื่อง

429 = คุณยิงเกินโควตาของตัวเอง แก้ที่การจัดจังหวะฝั่งคุณ · 503 = ระบบกำลังลดโหลด แก้ด้วยการรอตาม Retry-After · เมื่อระบบตึง การสร้างรายการฝากใหม่คือสิ่งแรกที่ถูกลดโหลด เพราะยังไม่มีใครโอนเงิน ⇒ ไม่มีใครเสียหาย ส่วนการอัปสลิปจะไม่ถูกลดโหลดเลย


ต้องเก็บอะไรลง DB ​

ตอนไหนเก็บอะไร
ก่อนยิงmerchant_reference (UNIQUE) · Idempotency-Key (UNIQUE)
หลังได้ 201/202deposit_id (ใช้เป็น PK ฝั่งคุณ) · version · status · expires_at · snapshot ของ sender.* แบบ masked
หน้าโอนเก็บแค่ expires_at · 🔴 qr_payload และเลขบัญชีปลายทางเต็ม ห้ามเก็บ ห้าม log

คอลัมน์ deposit_id ต้องเป็น text

ถ้าคุณย้ายมาจาก gateway เจ้าอื่น อย่าใช้ varchar(20) และอย่า parse prefix — รูปแบบ id ของเราต่างจากเดิมและอาจเปลี่ยนความยาวได้


ขั้นต่อไป ​

เอกสารชุดนี้อิงสัญญา API เวอร์ชัน 2026-08-01