Skip to content

ตั้งค่า ​

http
GET   /v1/bo/settings
PATCH /v1/bo/settings

หน้าจอ Integration ที่เปิดใช้จริง

การอ่าน health แบบ redacted และการเปลี่ยน webhook endpoint revision ใช้ GET /v1/bo/integrations กับ PUT /v1/bo/webhooks/{webhook_id} ดูสัญญา transport, CAS, step-up และ URL policy ที่ Integration และ Webhook

หลักการเดียวที่ครอบทุกค่า — ตั้งเข้มขึ้นได้ ตั้งหลวมกว่าไม่ได้

ทุกค่าในหน้านี้ถูก clamp ด้วยนโยบายของชั้นบน ส่งค่าที่หลวมกว่าเพดานไปจะ ไม่ error แต่จะถูกลดลงมาให้อยู่ในเพดานเงียบ ๆ ⇒ 🔴 อ่านค่าที่ระบบคืนกลับมาเสมอ ห้ามสมมติว่าค่าที่ส่งไปคือค่าที่ได้

3 ระดับของการตั้งค่า ​

ระดับตั้งที่ไหนตัวอย่าง
ต่อ requestใส่ในตัว request แต่ละใบอายุรายการ · ช่องทางที่ยอมรับ · metadata
ต่อบัญชีPATCH /v1/bo/settings และหน้าตั้งค่า webhookURL ปลายทาง webhook · เวอร์ชัน API · ผู้ติดต่อทางเทคนิค
เพดานของชั้นบนปรับไม่ได้รอบ retry · หน้าต่าง 72 ชั่วโมง · โควตา rate limit

ระดับที่ 1 — ค่าที่ส่งไปกับ request ทีละใบ ​

ค่าเหล่านี้ ไม่ต้องตั้งล่วงหน้า ส่งไปตอนสร้างรายการเลย

json
{
  "amount_minor": 500000,
  "currency": "THB",
  "merchant_reference": "ORD-9981",
  "end_user_ref": "u_88213",
  "sender_id": "snd_01K2R4M8N0PZ3XT5VKD9WY2C7Q",
  "client": {
    "device_fingerprint": "df_9a1c4e77b2",
    "ip": "203.0.113.44",
    "user_agent": "Mozilla/5.0"
  },
  "fill_preference": "any",
  "min_ttl_seconds": 300,
  "expires_in_seconds": 1200,
  "return_url": "https://shop.example.com/topup/9981",
  "metadata": { "campaign": "aug-cashback", "order_id": "ORD-9981" }
}
ฟิลด์ค่าหมายเหตุ
expires_in_secondsวินาที🔴 ถูก clamp ด้วยนโยบายของชั้นบน · อ่าน expires_at ที่คืนมา
min_ttl_seconds0–86,400 วินาทีถ้าจับคู่ได้แต่เวลาเหลือน้อยกว่านี้ ให้ข้ามไปหาช่องทางอื่นแทน; ถ้าไม่พอดีกับอายุรายการจะได้ 422
fill_preferenceany (ค่าเริ่มต้น) หรือ internal_onlyinternal_only = ไม่ใช้ช่องทางสำรอง ⇒ โอกาสอยู่ในสถานะรอสภาพคล่องนานขึ้น
return_urlhttps://หน้าที่ผู้ใช้กลับมาหลังจ่ายบนหน้าชำระเงินสำเร็จรูป
metadataobjectไม่เกิน 20 key · แต่ละค่าไม่เกิน 500 ไบต์

client{} ไม่ใช่ optional ในทางปฏิบัติ

ในเอกสารเขียนว่า optional แต่ผลของการไม่ส่งคือ

ส่ง client{}เพดานรายการค้างต่อผู้ใช้ถังของ rate limit
ครบ (device fingerprint + IP)2 ใบ ต่อผู้ใช้แต่ละคนpay_create_per_user 10 ครั้ง/นาที ต่อผู้ใช้
ไม่ส่ง🔴 เหลือ 1 ใบ🔴 ตกไปถังหยาบที่ แชร์กับผู้ใช้ทุกคนของคุณ ⇒ ชน 429 ตั้งแต่นาทีแรก

เหตุผล: ถ้าไม่มี device fingerprint ค่า end_user_ref เป็นสิ่งที่คุณกรอกเองและปลอมได้ฟรี เราจึงต้องรัดเพดานลง

fee_breakdown[] แยกตาม component ไม่ใช่ตามผู้รับ

fee_breakdown[] ที่ติดมากับ deposit.settled มีรูปแบบ {component, amount_minor, rate_bps?} เช่น processing_fee · fixed_fee · vatcomponent เป็น open enum ⇒ switch ของคุณต้องมี default: ที่บันทึกไว้แล้ว alert

ระดับที่ 2 — ตั้งค่าต่อบัญชี ​

ปลายทาง webhook ​

ค่าบังคับถ้าไม่ตั้ง
deposit_url✅—
withdrawal_url❌event ของรายการถอนไปที่ deposit_url
account_url❌event balance.* และ webhook.* ไปที่ deposit_url

ข้อกำหนดของ URL ที่ปฏิเสธไม่ได้

  • ต้องเป็น https:// · TLS ตั้งแต่ 1.2 ขึ้นไป · ใบรับรองต้องใช้ได้จริง (self-signed = ไม่ผ่าน)
  • 🔴 เราไม่ตาม redirect — ตอบ 3xx นับเป็นล้มเหลว (กัน SSRF และกันลายเซ็นไปโผล่ที่อื่น)
  • เราไม่เติม query parameter ใด ๆ และไม่ส่งข้อมูลใน query string เลย
  • ตอบ 2xx ภายใน 3 วินาที (ค่าที่แนะนำ) · เพดานจริง 15 วินาที
  • 🔴 ห้ามพึ่ง IP allowlist — เราไม่มี IP ต้นทางคงที่

5 event ที่ยกเลิกการรับไม่ได้

deposit.reversed · withdrawal.fill_reversed · balance.debited · webhook.key.rotating · webhook.endpoint.disabled

สามตัวแรกคือ การเอาเงินออกจากกระเป๋าคุณ ถ้าปิดได้ คุณจะเห็นแต่ขาเข้าและกระทบยอดไม่ได้เลย บังคับที่ระดับฐานข้อมูล ไม่ใช่แค่เขียนไว้ในเอกสาร — endpoint ที่ subscribe ไม่ครบจะบันทึกไม่ได้

เวอร์ชัน API ​

ส่งเป็น header PG-Version: 2026-08-01 ทุก request และมันอยู่ใน canonical string ของลายเซ็นด้วย

เรื่องนโยบาย
รูปแบบYYYY-MM-DD
อัปเกรดopt-in — เราไม่ย้ายให้เอง
breaking changeประกาศล่วงหน้า 90 วัน · รองรับเวอร์ชันเก่า 12 เดือน · มี header X-PG-Deprecation

สิ่งเหล่านี้ ไม่นับ เป็น breaking change

  • เพิ่ม field ใหม่ใน payload
  • เพิ่มค่าใหม่ใน enum ที่ประกาศว่าเป็น open enum: reasons[].code · rejection.code · balance_transaction.type · fee_breakdown[].component · EventType

⇒ 🔴 ทุก switch ที่แตะค่าเหล่านี้ ต้องมี default: ที่บันทึกไว้แล้ว alert ไม่ใช่ throw และไม่ใช่เงียบ โค้ดที่ throw ใน default: จะทำให้ endpoint ตอบ non-2xx ⇒ เข้ารอบ retry ⇒ ยอด delivery ล้มเหลวพุ่งจนระบบปิด endpoint ให้อัตโนมัติ

api_version ของ event ที่ส่งออก render ตามเวอร์ชันที่คุณ pin ไว้ ณ ตอนสร้าง event ⇒ retry ของ event เก่าใช้เวอร์ชันเดิมเสมอ

กุญแจตรวจลายเซ็น ​

http
GET https://api.example-wl.com/v1/webhooks/signing-keys
  • ไม่ต้อง auth (public key เป็นของสาธารณะอยู่แล้ว) · โควตา 60 ครั้ง/นาที/IP
  • แคชได้ไม่เกิน 24 ชั่วโมง · เจอ kid ที่ไม่รู้จัก → ดึงใหม่ 1 ครั้ง (หน่วง 1 ครั้ง/นาที) ก่อนตัดสินว่าไม่ผ่าน
  • pin ลง ENV ได้ แต่ ต้อง subscribe webhook.key.rotating ซึ่งประกาศ kid ใหม่ล่วงหน้า 7 วัน

เราเซ็นด้วยกุญแจที่ active ณ วินาทีที่ส่ง ไม่ใช่กุญแจตอนสร้าง event

เพราะ t ในลายเซ็นต้องใหม่ทุกครั้ง ⇒ merchant ที่ยังไม่รับกุญแจใหม่จะ verify ไม่ผ่าน ทันที ที่มีการสลับกุญแจ รวมถึง event เก่าที่ยัง retry อยู่ นี่คือเหตุผลที่ webhook.key.rotating ปิดไม่ได้

ผู้ติดต่อทางเทคนิค ​

ต้องเป็นอีเมลที่มีคนอ่านจริง — ใช้แจ้งกรณี endpoint ล่มยาวและกรณีระบบปิด endpoint ให้อัตโนมัติ ถ้าคุณมี endpoint เดียวและมันถูกปิด event ที่ปิดไม่ได้จะถูกส่งทางอีเมลไปที่นี่

Sandbox กับ live ​

  • ทุก event มีฟิลด์ livemode · false = sandbox
  • 🔴 event ของ sandbox ไม่มีวันถึง endpoint ของ live และกลับกัน — บังคับที่ระดับฐานข้อมูล ไม่ใช่ตรรกะในโค้ด
  • ตรวจ livemode ให้ตรงกับ environment ของคุณเป็นเงื่อนไข go-live

ระดับที่ 3 — เพดานที่ชั้นบนกำหนด (ปรับไม่ได้) ​

เรื่องค่าผลถ้าชน
รายการฝากที่ยังไม่จ่าย ต่อผู้ใช้2 ใบ (1 ใบถ้าไม่ส่ง client{})409 too_many_open_deposits + details.open_deposit_ids[]
รายการยอดเดียวกัน + ผู้โอนเดียวกันที่ค้างอยู่ห้ามซ้ำ409 duplicate_open_deposit + details.open_deposit_ids[]
ยอดต่ำสุด/สูงสุดตามนโยบาย422 amount_not_supported + details.min_minor / details.max_minor
เศษสตางค์ในยอดรายการฝาก🔴 ไม่รับ ต้องเป็นยอดกลม422 amount_not_supported
จำนวนก้อนสูงสุดต่อ 1 รายการถอนอ่านจาก max_splits ใน object ของใบนั้นระบบจัดการให้เอง
หน้าต่างรอพ้นข้อพิพาท72 ชั่วโมงเงินอยู่ในช่อง pending_clearing
หน้าต่างรับสลิปย้อนหลัง24 ชั่วโมงหลัง expires_at410 late_slip_window_closed + claim_url
เวลารออนุมัติรายการถอน24 ชั่วโมงยกเลิกอัตโนมัติ cancel_reason_code: "approval_ttl_expired"
รอบ retry ของ webhook11 ครั้ง ~92 ชั่วโมง 36 นาทีเลิก retry แล้วเก็บ event ไว้ 30 วันให้ resend
อายุของ Idempotency-Key7 วัน🔴 TTL ฝั่งคุณ ห้ามสั้นกว่านี้
ไฟล์สลิปชนิด/ขนาดตาม slip_upload.content_types / slip_upload.max_bytes ของ capability ใบนั้น400 unsupported_media_type / 422 validation_error
ย้อนหลังของ /v1/pay/events30 วันดึงเก่ากว่านี้ไม่ได้

🔴 ยอดรายการฝากต้องเป็นยอดกลม ห้ามมีเศษสตางค์

ถ้าระบบเดิมของคุณใช้เทคนิค "บวกเศษสตางค์สุ่มเพื่อแยกรายการ" (amount + random_cents) ต้องลบทิ้ง เราจับคู่ด้วย ตัวตนของผู้โอน ไม่ใช่ด้วยเศษสตางค์ ⇒ ยอดที่มีเศษจะได้ 422 amount_not_supported และตัว matcher เดิมที่จับสลิปจากเศษสตางค์จะใช้ไม่ได้อีกเลย

โควตา rate limit ​

ทุก response มี header ชุดนี้ ไม่ใช่เฉพาะตอนโดน 429

RateLimit-Limit: 600
RateLimit-Remaining: 587
RateLimit-Reset: 41
RateLimit-Policy: 600;w=60
X-RateLimit-Scope: api_key
Retry-After: 41

ตัวอย่างด้านบนเป็น merchant API ปกติจึงได้ X-RateLimit-Scope: api_key; slip ingress ใช้ scope จาก signed rate_subject/jti + IP ใน capability และ /v1/bo/* ใช้ employee OIDC tenant + principal — ไม่มีสองเส้นนี้ที่ fallback ไป bucket API key

ถังขอบเขตเพดานหน้าต่าง
pay_write (สร้างรายการฝาก/ถอน)API key60060 วินาที
pay_create_per_userผู้ใช้ + device + IP1060 วินาที
pay_readAPI key1,20060 วินาที
slip_uploadsigned rate_subject/jti + IP จาก capability1,20060 วินาที
slip_upload_per_usersigned rate_subject + IP จาก capability2060 วินาที
bo_reademployee OIDC tenant + principal30060 วินาที
bo_reportsemployee OIDC tenant + principal6060 วินาที
webhook_resend (นับ คำสั่ง ไม่ใช่ delivery)endpoint1060 วินาที
banks_quoteAPI key3,00060 วินาที

429 กับ 503 ไม่ใช่เรื่องเดียวกัน

  • 429 rate_limited = คุณ ยิงเกินโควตาของตัวเอง ⇒ เคารพ Retry-After + jitter ±20% · 🔴 ห้าม retry เป็นลูป มีบทลงโทษทวีคูณ
  • 503 service_unavailable / 503 route_unavailable = ระบบ กำลังลดโหลด ⇒ ไม่ใช่บั๊กของคุณ ไม่ใช่ 500

429 ไม่ยึด Idempotency-Key ⇒ ยิงซ้ำด้วย key เดิมได้ทันทีหลัง Retry-After

สิ่งที่เราไม่เปิดให้ตั้ง — และเหตุผล ​

อยากตั้งได้ไหมเพราะ
ให้ webhook มาครั้งเดียวตอนรายการถอนจบ❌แต่ละก้อนมาจากผู้โอนคนละคน ⇒ statement ธนาคารของคุณมาเป็นรายบรรทัด ถ้ายิงก้อนเดียวคุณกระทบยอดไม่ได้
ปิดหน้าต่าง 72 ชั่วโมงเพื่อถอนได้ทันที❌มันคือสิ่งที่ทำให้การกลับรายการตัดจาก pending_clearing แทนที่จะทำให้กระเป๋าคุณติดลบ
ให้ระบบอนุมัติรายการที่ค้างคิวตรวจสอบเองเมื่อเกินเวลา❌ไม่มี event ชื่อ deposit.auto_approved และจะไม่มี · เกินเวลาเราส่ง deposit.under_review ซ้ำ ด้วย review.escalated: true และ version ใหม่
เลือกให้ระบบใช้สภาพคล่องแหล่งไหนบางส่วนตั้งได้แค่ fill_preference ต่อ request · รายละเอียดที่ลึกกว่านั้นเป็นนโยบายภายใน
เห็นเวลาที่เจ้าหน้าที่ต้องตัดสินให้เสร็จ❌คิว support เป็นของผู้ให้บริการชั้นบน · สิ่งที่ส่งให้คุณคือ review.estimated_resolution_bucket (under_15m / under_1h / under_4h / extended)
ปรับข้อความที่แสดงเหตุผลการเข้าคิวตรวจสอบ❌เราส่ง display_th / display_en มาให้พร้อมใช้ ⇒ 🔴 อย่าเขียนข้อความเอง และ ห้าม hardcode "ไม่เกิน 15 นาที"

เช็กลิสต์การตั้งค่า ​

  • [ ] อ่านค่าที่ระบบคืนกลับมาหลัง PATCH ทุกครั้ง ไม่สมมติว่าได้ค่าที่ส่งไป
  • [ ] deposit_url ตั้งแล้ว และ endpoint ตอบ 2xx ภายใน 3 วินาที
  • [ ] subscribe ครบ 5 event ที่ปิดไม่ได้
  • [ ] pin PG-Version แล้วมีขั้นตอนอัปเกรดที่ทดสอบใน sandbox ก่อน
  • [ ] default: ครบทุก switch ของ open enum
  • [ ] TTL ของ idempotency ฝั่งคุณไม่สั้นกว่า 7 วัน
  • [ ] sk_live_* อยู่ใน secret manager หมุนได้โดยไม่ต้อง deploy ใหม่
  • [ ] ผู้ติดต่อทางเทคนิคเป็นอีเมลที่มีคนอ่านจริง
  • [ ] ส่ง client{} ครบทุกครั้ง

ขั้นต่อไป ​

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