Webhooks
Webhooks
เมื่อการชำระเงินสำเร็จ ระบบจะส่ง HTTP POST ไปยัง Webhook URL ที่ Merchant กำหนดไว้ เพื่อให้ Merchant อัปเดตข้อมูลในระบบของตนเอง
การตั้งค่า Webhook URL
ตั้งค่าได้ใน Admin Panel → Business Setup → Webhook Config → กด "Add Webhook"
ตัวอย่าง Webhook URL ของ Merchant: https://your-domain.com/webhooks/payment
Events
| Event | คำอธิบาย |
|---|---|
PaymentIn.Success |
ลูกค้าชำระเงิน Pay-In สำเร็จ เงินเข้าบัญชี Merchant แล้ว |
PaymentIn.Rejected |
Pay-In Request ถูกปฏิเสธ |
PaymentOut.Success |
ระบบโอนเงิน Pay-Out ออกสำเร็จ เงินถูกโอนไปยังบัญชีปลายทางแล้ว |
PaymentOut.Rejected |
Pay-Out Request ถูกปฏิเสธ |
Fields ใหม่ใน Payload
ทุก webhook payload ตั้งแต่เวอร์ชันนี้เป็นต้นไปจะมี parameter เพิ่มเติม 3 ตัว:
| Parameter | คำอธิบาย |
|---|---|
EVENT_TYPE |
ประเภทของ event เช่น PaymentIn.Success, PaymentIn.Rejected, PaymentOut.Success, PaymentOut.Rejected |
STATUS_CODE |
สถานะการทำงาน — เป็น OK เมื่อไม่มีปัญหา; สำหรับ Rejected events จะมีค่าไม่เท่ากับ OK |
STATUS_REASON |
คำอธิบายเหตุผลของ status — จะมีค่าเมื่อ STATUS_CODE ไม่เท่ากับ OK (เช่น Rejected events) |
PAYMENT_TYPE |
ประเภทของ payment — มีค่าเป็น PayIn หรือ PayOut เท่านั้น |
หมายเหตุ: พวก event ที่เป็น Rejected (
PaymentIn.Rejected,PaymentOut.Rejected) จะมีSTATUS_CODEไม่เท่ากับ"OK"เสมอ ใช้ field นี้เพื่อเช็คว่าการทำงานมีปัญหาหรือไม่
รูปแบบ Payload
PaymentIn.Success
{
"Id": "job-uuid",
"Type": "PaymentIn.Success",
"Parameters": [
{ "Name": "EVENT_TYPE", "Value": "PaymentIn.Success" },
{ "Name": "STATUS_CODE", "Value": "OK" },
{ "Name": "PAYMENT_TYPE", "Value": "PayIn" },
{ "Name": "ORG_ID", "Value": "org-id" },
{ "Name": "PMR_ID", "Value": "payment-request-uuid" },
{ "Name": "PMR_REF_ID1", "Value": "ORDER-20260701-001" },
{ "Name": "PMR_REF_ID2", "Value": "CUST-12345" },
{ "Name": "PMR_REF_ID3", "Value": null },
{ "Name": "MERCHANT_ID", "Value": "merchant-uuid" },
{ "Name": "MERCHANT_CODE", "Value": "merchant-code" },
{ "Name": "MERCHANT_NAME", "Value": "ชื่อร้านค้า" },
{ "Name": "PAYIN_REQUEST_AMOUNT", "Value": "325" },
{ "Name": "PAYIN_GENERATED_AMOUNT", "Value": "325.52" },
{ "Name": "PAYIN_FEE_PCT", "Value": "0" },
{ "Name": "PAYIN_BANK_CODE", "Value": "SCB" },
{ "Name": "PAYIN_BANK_ACCOUNT_NO", "Value": "xxx-xxxxx-x" },
{ "Name": "PAYIN_BANK_ACCOUNT_NAME", "Value": "ชื่อบัญชี" }
]
}
Field ที่สำคัญ
| Parameter | คำอธิบาย |
|---|---|
EVENT_TYPE |
PaymentIn.Success |
STATUS_CODE |
OK |
PAYMENT_TYPE |
PayIn |
PMR_REF_ID1 |
Ref 1 — Reference ID ที่ Merchant ส่งมาตอนสร้าง Payment Request ใช้สำหรับ mapping กับ order ในระบบของ Merchant |
PMR_REF_ID2 |
Ref 2 — optional reference ที่ Merchant กำหนด |
PMR_REF_ID3 |
Ref 3 — optional reference ที่ Merchant กำหนด |
PMR_ID |
UUID ของ Payment Request ในระบบ |
PAYIN_REQUEST_AMOUNT |
จำนวนเงินที่ขอตั้งต้น |
PAYIN_GENERATED_AMOUNT |
จำนวนเงินที่รับจริง (อาจมีเศษสตางค์ต่างกัน) |
PaymentIn.Rejected
{
"Id": "job-uuid",
"Type": "PaymentIn.Rejected",
"Parameters": [
{ "Name": "EVENT_TYPE", "Value": "PaymentIn.Rejected" },
{ "Name": "STATUS_CODE", "Value": "REJECTED" },
{ "Name": "STATUS_REASON", "Value": "เหตุผลที่ปฏิเสธ" },
{ "Name": "PAYMENT_TYPE", "Value": "PayIn" },
{ "Name": "ORG_ID", "Value": "org-id" },
{ "Name": "PMR_ID", "Value": "payment-request-uuid" },
{ "Name": "PMR_REF_ID1", "Value": "ORDER-20260701-001" },
{ "Name": "PMR_REF_ID2", "Value": "CUST-12345" },
{ "Name": "PMR_REF_ID3", "Value": null },
{ "Name": "MERCHANT_ID", "Value": "merchant-uuid" },
{ "Name": "MERCHANT_CODE", "Value": "merchant-code" },
{ "Name": "MERCHANT_NAME", "Value": "ชื่อร้านค้า" },
{ "Name": "PAYIN_REQUEST_AMOUNT", "Value": "325" },
{ "Name": "PAYIN_BANK_CODE", "Value": "SCB" },
{ "Name": "PAYIN_BANK_ACCOUNT_NO", "Value": "xxx-xxxxx-x" },
{ "Name": "PAYIN_BANK_ACCOUNT_NAME", "Value": "ชื่อบัญชี" }
]
}
Field ที่สำคัญ
| Parameter | คำอธิบาย |
|---|---|
EVENT_TYPE |
PaymentIn.Rejected |
STATUS_CODE |
ไม่เท่ากับ OK — ใช้เช็คว่ามีปัญหา |
STATUS_REASON |
เหตุผลที่ปฏิเสธ |
PAYMENT_TYPE |
PayIn |
PMR_ID |
UUID ของ Pay-In Request ที่ถูกปฏิเสธ |
PaymentOut.Success
{
"Id": "job-uuid",
"Type": "PaymentOut.Success",
"Parameters": [
{ "Name": "EVENT_TYPE", "Value": "PaymentOut.Success" },
{ "Name": "STATUS_CODE", "Value": "OK" },
{ "Name": "PAYMENT_TYPE", "Value": "PayOut" },
{ "Name": "ORG_ID", "Value": "org-id" },
{ "Name": "PMT_ID", "Value": "payment-tx-uuid" },
{ "Name": "PMR_ID", "Value": "payment-request-uuid" },
{ "Name": "PMR_REF_ID1", "Value": "260802110325" },
{ "Name": "PMR_REF_ID2", "Value": "CUST-12345" },
{ "Name": "PMR_REF_ID3", "Value": null },
{ "Name": "MERCHANT_ID", "Value": "merchant-uuid" },
{ "Name": "MERCHANT_CODE", "Value": "merchant-code" },
{ "Name": "MERCHANT_NAME", "Value": "ชื่อร้านค้า" },
{ "Name": "TX_AMOUNT", "Value": "1000.00" },
{ "Name": "PAYOUT_REQUEST_AMOUNT", "Value": "1000" },
{ "Name": "PAYOUT_FEE", "Value": "5.00" },
{ "Name": "PAYOUT_FEE_PCT", "Value": "0.5" },
{ "Name": "PAYOUT_BANK_CODE", "Value": "KBank" },
{ "Name": "PAYOUT_BANK_ACCOUNT_NO", "Value": "xxx-xxxxx-x" },
{ "Name": "PAYOUT_BANK_ACCOUNT_NAME", "Value": "ชื่อบัญชีปลายทาง" },
{ "Name": "PAYOUT_PROMPTPAY_ID", "Value": null },
{ "Name": "PAYOUT_PAID_AMOUNT_INCLUSIVE", "Value": "1000.00" },
{ "Name": "PAYOUT_IS_PARTIAL", "Value": "False" }
]
}
Field ที่สำคัญ
| Parameter | คำอธิบาย |
|---|---|
EVENT_TYPE |
PaymentOut.Success |
STATUS_CODE |
OK |
PAYMENT_TYPE |
PayOut |
PMR_REF_ID1 |
Ref 1 (auto-generated YYMMDDHHMMSS) |
PMR_REF_ID2 |
Ref 2 — optional reference ที่ Merchant กำหนด |
PMR_REF_ID3 |
Ref 3 — optional reference ที่ Merchant กำหนด |
PMR_ID |
UUID ของ Pay-Out Request ในระบบ |
TX_AMOUNT |
จำนวนเงินที่โอนออกจริงในรอบนี้ (transaction เดียว) |
PAYOUT_REQUEST_AMOUNT |
จำนวนเงินที่ขอโอนตั้งต้น |
PAYOUT_FEE |
ค่าธรรมเนียม |
PAYOUT_BANK_CODE |
รหัสธนาคารปลายทาง |
PAYOUT_PAID_AMOUNT_INCLUSIVE |
ยอดรวมสะสมที่ payout ออกไปแล้วทั้งหมดของ Pay-Out Request นี้ (รวมทุกรอบจนถึงรอบปัจจุบัน) — ต่างจาก TX_AMOUNT ที่เป็นยอดของรอบนี้รอบเดียว |
PAYOUT_IS_PARTIAL |
True ถ้าเป็น P2P partial payout (ดูด้านล่าง) |
PaymentOut.Success กับรายการ P2P
สำหรับ Pay-Out Request ที่ถูกจับคู่กับ P2P Pay-In (PAYOUT_IS_PARTIAL = "True") webhook PaymentOut.Success อาจถูกส่งมากกว่าหนึ่งครั้งสำหรับ Pay-Out Request เดียวกัน — เพราะ Pay-Out หนึ่งรายการอาจถูกแบ่งชำระจาก P2P Pay-In หลายรายการ (partial payouts)
ตัวอย่าง: Pay-Out Request 10,000 บาท อาจถูก fulfill จาก P2P Pay-In 3 รอบ:
| รอบ | TX_AMOUNT (ยอดรอบนี้) | PAYOUT_PAID_AMOUNT_INCLUSIVE (ยอดสะสม) | PAYOUT_IS_PARTIAL |
|---|---|---|---|
| รอบที่ 1 | 4,000 บาท | 4,000 บาท | True |
| รอบที่ 2 | 3,500 บาท | 7,500 บาท | True |
| รอบที่ 3 | 2,500 บาท | 10,000 บาท | True |
สิ่งที่ต้องทำในระบบของ Merchant:
- ตรวจสอบ
PAYOUT_IS_PARTIALก่อนเสมอ - ถ้า
True— อย่า mark Pay-Out ว่า "สำเร็จ" ทันที เทียบPAYOUT_PAID_AMOUNT_INCLUSIVEกับPAYOUT_REQUEST_AMOUNTแทนการรวมTX_AMOUNTเองทีละรอบ (ระบบสะสมยอดให้แล้วในฟิลด์นี้) - ถ้า
False— Pay-Out สำเร็จในครั้งเดียว สามารถ mark ว่า "สำเร็จ" ได้ทันที
# ตัวอย่าง: รับ PaymentOut.Success แบบ P2P
elif event_type == 'PaymentOut.Success':
paid_inclusive = float(params.get('PAYOUT_PAID_AMOUNT_INCLUSIVE', 0))
requested_amount = float(params.get('PAYOUT_REQUEST_AMOUNT', 0))
is_partial = params.get('PAYOUT_IS_PARTIAL', 'False') == 'True'
pmr_id = params.get('PMR_ID')
if is_partial:
# P2P partial — เช็คยอดสะสมที่ระบบส่งมาให้ตรง ๆ ไม่ต้องรวมเอง
if paid_inclusive >= requested_amount:
mark_payout_completed(pmr_id)
else:
# จ่ายครบรอบเดียว
mark_payout_completed(pmr_id)
PaymentOut.Rejected
{
"Id": "job-uuid",
"Type": "PaymentOut.Rejected",
"Parameters": [
{ "Name": "EVENT_TYPE", "Value": "PaymentOut.Rejected" },
{ "Name": "STATUS_CODE", "Value": "REJECTED" },
{ "Name": "STATUS_REASON", "Value": "เหตุผลที่ปฏิเสธ" },
{ "Name": "PAYMENT_TYPE", "Value": "PayOut" },
{ "Name": "ORG_ID", "Value": "org-id" },
{ "Name": "PMR_ID", "Value": "payment-request-uuid" },
{ "Name": "PMR_REF_ID1", "Value": "260802110325" },
{ "Name": "PMR_REF_ID2", "Value": "CUST-12345" },
{ "Name": "PMR_REF_ID3", "Value": null },
{ "Name": "MERCHANT_ID", "Value": "merchant-uuid" },
{ "Name": "MERCHANT_CODE", "Value": "merchant-code" },
{ "Name": "MERCHANT_NAME", "Value": "ชื่อร้านค้า" },
{ "Name": "PAYOUT_REQUEST_AMOUNT", "Value": "1000" },
{ "Name": "PAYOUT_BANK_CODE", "Value": "KBank" },
{ "Name": "PAYOUT_BANK_ACCOUNT_NO", "Value": "xxx-xxxxx-x" },
{ "Name": "PAYOUT_BANK_ACCOUNT_NAME", "Value": "ชื่อบัญชีปลายทาง" }
]
}
Field ที่สำคัญ
| Parameter | คำอธิบาย |
|---|---|
EVENT_TYPE |
PaymentOut.Rejected |
STATUS_CODE |
ไม่เท่ากับ OK — ใช้เช็คว่ามีปัญหา |
STATUS_REASON |
เหตุผลที่ปฏิเสธ |
PAYMENT_TYPE |
PayOut |
PMR_ID |
UUID ของ Pay-Out Request ที่ถูกปฏิเสธ |
ตัวอย่างการรับ Webhook
Python (Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/payment', methods=['POST'])
def handle_webhook():
data = request.get_json()
event_type = data.get('Type')
# แปลง Parameters array เป็น dict
params = {p['Name']: p['Value'] for p in data.get('Parameters', [])}
ref_id = params.get('PMR_REF_ID1')
status_code = params.get('STATUS_CODE')
payment_type = params.get('PAYMENT_TYPE')
if event_type == 'PaymentIn.Success':
amount = params.get('PAYIN_REQUEST_AMOUNT')
update_order_status(ref_id, 'paid_in', amount)
elif event_type == 'PaymentIn.Rejected':
reason = params.get('STATUS_REASON')
update_order_status(ref_id, 'rejected', reason=reason)
elif event_type == 'PaymentOut.Success':
amount = params.get('TX_AMOUNT')
update_order_status(ref_id, 'paid_out', amount)
elif event_type == 'PaymentOut.Rejected':
reason = params.get('STATUS_REASON')
update_order_status(ref_id, 'payout_rejected', reason=reason)
return jsonify({'status': 'ok'}), 200
Node.js (Express)
app.post('/webhooks/payment', express.json(), (req, res) => {
const { Type, Parameters } = req.body
const params = Object.fromEntries(Parameters.map(p => [p.Name, p.Value]))
const refId = params.PMR_REF_ID1
if (Type === 'PaymentIn.Success') {
updateOrderStatus(refId, 'paid_in', params.PAYIN_REQUEST_AMOUNT)
} else if (Type === 'PaymentIn.Rejected') {
updateOrderStatus(refId, 'rejected', null, params.STATUS_REASON)
} else if (Type === 'PaymentOut.Success') {
updateOrderStatus(refId, 'paid_out', params.TX_AMOUNT)
} else if (Type === 'PaymentOut.Rejected') {
updateOrderStatus(refId, 'payout_rejected', null, params.STATUS_REASON)
}
res.json({ status: 'ok' })
})
Response ที่ merchant ต้องตอบกลับ
เมื่อระบบยิง webhook ไปหา merchant แล้ว ฝั่ง merchant ต้องตอบกลับเป็น JSON ที่มีฟีลด์ status เสมอ เพื่อให้ระบบรู้ว่า merchant รับข้อมูลและประมวลผลสำเร็จจริง ไม่ใช่แค่ได้รับ request เฉย ๆ
{ "status": "ok" }
ระบบตรวจสอบผลลัพธ์ตามลำดับนี้:
- HTTP status ที่ไม่ใช่
20X→ ถือว่า failed ทั้งหมด (ไม่ต้องดู response body ต่อ) - ถ้าเป็น
20Xระบบจะแกะ response body เป็น JSON แล้วเช็คฟีลด์statusต่อ- ต้องเป็น
successหรือOK(ไม่สนตัวพิมพ์ใหญ่/เล็ก) จึงจะถือว่า สำเร็จ - ถ้า response ไม่ใช่ JSON ที่แกะได้ หรือฟีลด์
statusไม่ใช่success/OK→ ถือว่า failed เช่นกัน
- ต้องเป็น
หาก failed ไม่ว่ากรณีใดก็ตาม (webhook config ไม่ได้ตั้งค่าไว้, ยิงไม่ถึง merchant, HTTP status ไม่ใช่ 20X, หรือ 20X แต่ response ไม่ผ่านเงื่อนไขข้างต้น) ระบบจะสร้าง AuditNotice (warning) ไว้ที่ Payment Request / Payment Transaction รายการนั้น — สังเกตได้จากไอคอน ⚠️ ในคอลัมน์ Status บน Admin Portal และ Merchant Portal กดไอคอนเพื่อดูรายละเอียดข้อผิดพลาด และระบบจะแจ้งกลับไปยัง merchant ด้วย
ข้อควรทราบ
- ตอบกลับด้วย HTTP
200และ JSON{ "status": "ok" }เพื่อยืนยันว่าได้รับและประมวลผล webhook สำเร็จ (ดูรายละเอียดด้านบน) - ระบบยังไม่มี retry policy — หาก webhook ล้มเหลว ข้อมูลจะไม่ถูกส่งซ้ำ
- ระบบยังไม่มี signature verification — แนะนำให้ตรวจสอบ
PMR_REF_ID1ว่าตรงกับ order ในระบบก่อนทำรายการ