Merchant Reports API
API รายงานสำหรับ ERP ทั้งรายงานรายการ ยอดคงเหลือรายวัน รายการคืนเงิน และ webhook report.daily_summary
สร้างจาก merchant-reports-api.md โดยอัตโนมัติ
ช่องทางให้ระบบอื่นดึงรายงานชุดเดียวกับที่ร้านค้าเห็นในแดชบอร์ด ได้แก่ ประวัติการรับเงิน รายงานยอดคงเหลือรายวัน รายการเคลื่อนไหวของแต่ละวัน การคืนเงิน และสรุปรายวัน ใช้ API key และลายเซ็น HMAC ชุดเดียวกับ API รับ-จ่ายเงิน ระบบ ERP จึงไม่ต้องล็อกอินแดชบอร์ดเลย
มี webhook คู่กันชื่อ report.daily_summary ที่ส่งสรุปของแต่ละวันที่ปิดยอดแล้วไปยัง webhook URL ของคุณ
ไม่มีตัวเลขค่าธรรมเนียม ไม่ว่าจะเป็น response ไฟล์ส่งออก หรือ webhook จะไม่มีค่าธรรมเนียม อัตราค่าธรรมเนียม MDR หรือการแยกยอดก่อน/หลังหักอยู่เลย ทุกจำนวนเงินคือผลสุทธิที่เกิดกับยอดคงเหลือของคุณ
#โฮสต์ (Hosts)
| Environment | Base URL | ข้อมูล |
|---|---|---|
| Production | https://api.<domain>/v1/merchant-reports/ | ข้อมูลจริง |
| Sandbox | https://sandbox-api.<domain>/v1/merchant-reports/ | ข้อมูลทดสอบ |
API key เป็นตัวกำหนดว่าเป็นร้านค้าใดและใช้ข้อมูลชุดไหน หากส่ง merchant_id มาใน query string ระบบจะไม่สนใจค่านั้น
#การยืนยันตัวตน (Authentication)
ทุกคำขอต้องแนบ header สามตัวเดียวกับ API รับ-จ่ายเงิน:
| Header | ค่า |
|---|---|
X-API-Key | API key ของคุณ |
X-Timestamp | เวลา Unix หน่วยวินาที ต้องต่างจากนาฬิกาของเซิร์ฟเวอร์ไม่เกิน 5 นาที |
X-Signature | ผลลัพธ์ของ HMAC-SHA256(secret_key, METHOD + "|" + PATH + "|" + X-Timestamp) ในรูปเลขฐานสิบหกตัวพิมพ์เล็ก |
PATHคือเฉพาะส่วน path ของ URL ไม่รวมโฮสต์และไม่รวม query string เช่น/v1/merchant-reports/statement- คำขอแบบ GET ไม่มี body จึงไม่มี
|ต่อท้าย ส่วนกรณีที่มี body ทาง API รับ-จ่ายเงินจะต่อ"|" + bodyเพิ่ม - รายการ IP ที่คุณอนุญาตไว้ (whitelist) มีผลกับ API ชุดนี้ด้วย
ตัวอย่าง ต่อไปนี้คือคำขอที่เซ็นแล้วสำหรับ GET /v1/merchant-reports/daily-summary?date=2026-09-18 ที่ X-Timestamp: 1789776900 โดยใช้ secret sk_test_example:
string to sign : GET|/v1/merchant-reports/daily-summary|1789776900
X-Signature : b80b3ebf9bd996732d1cd7b65805446ceb57f775b4bed4b9c24ccf35fd61bfeeTS=$(date +%s); P=/v1/merchant-reports/daily-summary
SIG=$(printf 'GET|%s|%s' "$P" "$TS" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -s "https://api.<domain>$P?date=2026-09-18" -H "X-API-Key: $API_KEY" -H "X-Timestamp: $TS" -H "X-Signature: $SIG"ข้อผิดพลาดของการยืนยันตัวตน รหัสเหล่านี้ตอบ 401: MISSING_API_KEY, MISSING_SIGNATURE, MISSING_TIMESTAMP, INVALID_TIMESTAMP, TIMESTAMP_EXPIRED, INVALID_API_KEY และ INVALID_SIGNATURE ส่วนรหัสเหล่านี้ตอบ 403: MERCHANT_INACTIVE และ IP_NOT_WHITELISTED
#ขีดจำกัดการเรียกใช้ (Rate limit)
ร้านค้าแต่ละรายเรียก /v1/merchant-reports/* ได้ 60 ครั้งต่อนาที โควตานี้แยกจาก API รับ-จ่ายเงิน การดึงรายงานจึงไม่ทำให้การรับเงินของคุณช้าลง
เมื่อเกินขีดจำกัดจะได้ 429 RATE_LIMIT_EXCEEDED พร้อม Retry-After และทุก response จะมี X-RateLimit-Limit, X-RateLimit-Remaining และ X-RateLimit-Reset กำกับมาด้วย
#ข้อตกลงร่วม (Conventions)
- วันที่:
from,toและdateเป็นวันแบบปฏิทินรูปแบบYYYY-MM-DDตามเวลา Asia/Bangkok ช่วงวันที่นับรวมทั้งวันเริ่มและวันสิ้นสุด และกว้างได้ไม่เกิน 366 วัน - เวลา: timestamp ใน JSON เป็น RFC 3339 ตามเวลา Asia/Bangkok เช่น
2026-09-19T15:05:00+07:00ค่านี้เป็นเวลาแบบ ISO-8601 ตามมาตรฐาน ตัวแปลงวันที่ทั่วไปอ่านได้เหมือนเดิม ต่างจากรูปแบบZที่ใช้ก่อนวันที่ 20 ก.ย. 2026 เฉพาะส่วน offset เท่านั้น - จำนวนเงิน: เป็นตัวเลขใน JSON ทศนิยม 2 ตำแหน่ง สกุลเงินบาท
- การแบ่งหน้า:
pageค่าเริ่มต้นคือ 1 ส่วนpage_sizeค่าเริ่มต้นคือ 50 และสูงสุด 200 format:jsonเป็นค่าเริ่มต้นcsvเป็น UTF-8 พร้อม BOM เวลาอยู่ในรูปYYYY-MM-DD HH:MM:SSตามเวลากรุงเทพฯxlsxเป็นไฟล์ Excel ช่องจำนวนเงินเป็นตัวเลขจริง (#,##0.00) ช่องเวลาเป็นเซลล์วันที่-เวลาของกรุงเทพฯ และช่องวันที่เป็นเซลล์วันที่จริง- CSV และ XLSX บรรจุทุกแถวที่ตรงเงื่อนไข ไม่ใช่เฉพาะหน้าเดียว หากเกิน 100,000 แถวจะได้ 413
TOO_MANY_ROWSให้ย่อช่วงวันที่ลง ไฟล์ที่ได้จะมาพร้อมContent-Disposition: attachment; filename="<kind>_<from>_<to>.<ext>"
- ข้อผิดพลาด: อยู่ในรูป
{"code": "...", "message": "..."}รหัสที่เป็นไปได้คือINVALID_RANGE,INVALID_PARAM,TOO_MANY_ROWS, รหัสของการยืนยันตัวตนข้างต้น และUPSTREAM_UNAVAILABLE(502 เรียกซ้ำได้อย่างปลอดภัย)
#Endpoint
#GET /v1/merchant-reports/daily-summary?date=YYYY-MM-DD
คืนข้อมูลของวันตามเวลากรุงเทพฯ ที่ปิดยอดแล้วหนึ่งวัน ค่า date ต้องเป็นวันก่อนวันนี้ตามเวลากรุงเทพฯ มิฉะนั้นจะได้ 400 INVALID_RANGE
ประเภทรายการทั้งหกจะปรากฏเสมอ จำนวนเงินของแต่ละประเภทคือผลสุทธิต่อยอดคงเหลือพร้อมเครื่องหมาย เงินเข้าเป็นบวก ส่วนเงินออกและการตัดยอดเป็นลบ ตัวเลขจะกระทบยอดกันได้เสมอ: ผลรวมของ kinds.amount = closing_balance − opening_balance และ credits − debits = closing_balance − opening_balance
{
"merchant_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "date": "2026-09-18",
"timezone": "Asia/Bangkok", "currency": "THB",
"opening_balance": 1000.00, "closing_balance": 920.33, "credits": 1785.00, "debits": 1864.67,
"kinds": {
"payin": { "count": 2, "amount": 1485.00 },
"payout": { "count": 1, "amount": -300.00 },
"settlement": { "count": 1, "amount": -1010.00 },
"withdraw_returned": { "count": 1, "amount": 300.00 },
"payin_reversed": { "count": 1, "amount": -500.00 },
"adjustment": { "count": 2, "amount": -54.67 }
},
"generated_at": "2026-09-19T00:15:02+07:00"
}วันที่ไม่มีรายการเคลื่อนไหวจะได้ count เป็น 0 และ amount เป็น 0 ทุกประเภท และวันนั้นยอดยกมากับยอดยกไปจะเท่ากัน คือยอดคงเหลือล่าสุดก่อนหน้าวันนั้น
format=csv|xlsx จะได้ข้อมูลหนึ่งแถว ชื่อไฟล์ daily_summary_<date>.csv หรือ daily_summary_<date>.xlsx คอลัมน์ประกอบด้วย:
- Date, Opening balance, Money in, Money out
- คอลัมน์จำนวนครั้งและคอลัมน์จำนวนเงินของแต่ละประเภท: Pay-ins, Payouts, Settlements, Withdrawals returned, Pay-ins reversed, Adjustments
- Closing balance
#GET /v1/merchant-reports/transactions?from&to[&status&q&page&page_size&format]
คืนรายการรับเงินที่สร้างขึ้นในช่วงที่ระบุ เรียงจากใหม่ไปเก่า
statusเป็นค่าใดค่าหนึ่งใน pending, processing, pending_approval, completed, failed, expired, refunded, cancelledqใช้ค้นจากส่วนต้นของรหัสรายการ หรือบางส่วนของเลขที่คำสั่งซื้อ เลขอ้างอิงภายนอก หรือชื่อผู้ชำระเงิน
{
"data": [{
"id": "4f1c…", "order_id": "ORD-1", "amount": 1250.00, "status": "completed", "channel": "promptpay",
"payer_name": "Somchai", "payer_bank_code": "004", "payer_account_last4": "7890",
"created_at": "2026-09-19T15:05:00+07:00", "paid_at": "2026-09-19T15:06:00+07:00"
}],
"summary": { "total_count": 1, "completed_count": 1, "completed_amount": 1250.00, "pending_count": 0,
"failed_count": 0, "expired_count": 0, "refunded_count": 0,
"by_status": { "completed": 1, "pending": 0, "processing": 0, "pending_approval": 0,
"failed": 0, "expired": 0, "refunded": 0, "cancelled": 0 } },
"pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 }
}summary คำนวณจากช่วงวันที่และ q แต่ไม่สนใจ status
คอลัมน์ของ CSV และ XLSX คือ: Created at, Transaction ID, Order ID, Amount, Status, Channel, Payer name, Payer bank, Payer account (last 4), Paid at
#GET /v1/merchant-reports/statement?from&to[&format]
คืนรายงานยอดคงเหลือรายวัน หนึ่งแถวต่อหนึ่งวันตามเวลากรุงเทพฯ ที่มีรายการเคลื่อนไหว เรียงจากเก่าไปใหม่
{
"days": [{ "date": "2026-09-18", "opening_balance": 1000.00, "credits": 1785.00, "debits": 1864.67,
"closing_balance": 920.33, "settlement_count": 1, "settlement_amount": 1010.00, "entry_count": 8 }],
"totals": { "opening_balance": 1000.00, "credits": 1785.00, "debits": 1864.67, "closing_balance": 920.33,
"settlement_count": 1, "settlement_amount": 1010.00 }
}ที่นี่ settlement_amount เป็นค่าบวก ต่างจากในสรุปรายวันที่จำนวนเงินของการตัดยอดมีเครื่องหมายกำกับ
คอลัมน์ของ CSV และ XLSX คือ: Date, Opening balance, Money in, Money out, Settlements (count), Settlements (amount), Closing balance
#GET /v1/merchant-reports/statement/entries?date[&format]
คืนรายการเคลื่อนไหวของวันเดียว เรียงจากเก่าไปใหม่ ค่าของ kind ได้แก่ payin, payout, settlement, withdraw_returned, payin_reversed และ adjustment ส่วน reference คือเลขที่คำสั่งซื้อของคุณสำหรับรายการรับเงินหรือจ่ายเงิน เป็นเลข WDR… สำหรับการตัดยอด และเป็น null ในกรณีอื่น
{
"date": "2026-09-18", "opening_balance": 1000.00, "closing_balance": 920.33,
"entries": [{ "transaction_id": "…", "kind": "payin", "reference": "ORD-1", "amount": 985.00,
"balance_after": 1985.00, "created_at": "2026-09-18T00:10:00+07:00" }]
}คอลัมน์ของ CSV และ XLSX คือ: Time, Type, Reference, Transaction ID, Amount, Balance after ไฟล์ XLSX ใช้ชื่อ statement_entries_<date>.xlsx ส่วน CSV ยังคงใช้ statement_entries_<date>_<date>.csv
#GET /v1/merchant-reports/refunds?from&to[&q&page&page_size&format]
คืนรายการรับเงินที่ถูกคืนเงิน โดยนับจากวันที่คืนเงินอยู่ในช่วงที่ระบุ เรียงจากรายการที่คืนเงินล่าสุดก่อน
{
"data": [{ "id": "…", "order_id": "ORD-2", "amount": 500.00, "reason": "customer request",
"paid_at": "2026-09-18T08:00:00+07:00", "refunded_at": "2026-09-18T10:00:00+07:00" }],
"summary": { "count": 1, "amount": 500.00 },
"pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 }
}คอลัมน์ของ CSV และ XLSX คือ: Refunded at, Transaction ID, Order ID, Amount, Reason, Paid at
#Webhook report.daily_summary
#การสมัครรับ
สมัครรับได้ในแดชบอร์ดร้านค้าที่ ตั้งค่า › API ซึ่งต้องตั้ง webhook URL ไว้ก่อน หรือจะเพิ่ม report.daily_summary เข้าไปใน events ของการตั้งค่า webhook ของคุณก็ได้ ปุ่มทดสอบจะส่งข้อมูลของวันที่เลือกซ้ำทันที ได้สูงสุด 5 ครั้งต่อนาที รหัสของเหตุการณ์ทดสอบจะลงท้ายด้วย :test:<unix>
#ส่งเมื่อใด
สรุปจะถูกส่งวันละหนึ่งครั้งตามวันของกรุงเทพฯ โดยเป็นข้อมูลของวันก่อนหน้า ส่งเวลาประมาณ 00:15 น. ตามเวลา Asia/Bangkok และจะส่งแม้วันนั้นไม่มีรายการเคลื่อนไหวเลย เพื่อให้แยกออกว่า "วันนั้นไม่มีรายการ" ต่างจาก "ส่งไม่สำเร็จ"
หากมีการส่งตกหล่น เช่นช่วงปิดปรับปรุงระบบ วันใดในสามวันล่าสุดที่ยังไม่ได้ส่งจะถูกส่งในรอบถัดไป ข้อมูลที่ส่งเป็นของ environment ปัจจุบันของร้านค้าคุณ
#การนำส่ง
การนำส่งใช้กลไกเดียวกับ webhook ของรายการธุรกรรม คือ HTTPS POST ไปยัง webhook URL ของคุณ โดยเซ็นด้วย secret key ของ API ของคุณ
- ถือว่าส่งสำเร็จเมื่อคุณตอบกลับด้วยสถานะ 2xx ใดก็ได้ภายใน 10 วินาที
- หากส่งไม่สำเร็จจะลองใหม่ทุกหนึ่งนาที สูงสุด 5 ครั้ง
- วันเดียวกันจะไม่ถูกส่งซ้ำเป็นครั้งที่สอง
- ใช้
id(daily_summary:<merchant_id>:<date>) เป็นกุญแจกันการประมวลผลซ้ำ
#Header
Content-Type: application/json
X-Webhook-Event: report.daily_summary
X-Webhook-Timestamp: 1789828523
X-Webhook-Signature: a0f17280f569734ab64b5b33968c21933fcd737533cd7683f0785f74236532b2
X-Transaction-ID: d51ab35b-3111-51fd-a017-27d373a55d35X-Transaction-IDคือกุญแจของการนำส่ง เป็น UUIDv5 ที่สร้างจากidและมีค่าเท่าเดิมทุกครั้งที่ลองส่งใหม่timestampใน body คือเวลาของการส่งครั้งนั้น จึงเปลี่ยนไปในการส่งซ้ำแต่ละครั้ง
#Body
data คือ response ของสรุปรายวันทุกประการ คีย์เรียงตามตัวอักษร และตัวเลขมีทศนิยม 2 ตำแหน่ง
{"data":{"closing_balance":920.33,"credits":1785.00,"currency":"THB","date":"2026-09-18","debits":1864.67,"generated_at":"2026-09-19T00:15:02+07:00","kinds":{"adjustment":{"amount":-54.67,"count":2},"payin":{"amount":1485.00,"count":2},"payin_reversed":{"amount":-500.00,"count":1},"payout":{"amount":-300.00,"count":1},"settlement":{"amount":-1010.00,"count":1},"withdraw_returned":{"amount":300.00,"count":1}},"merchant_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","opening_balance":1000.00,"timezone":"Asia/Bangkok"},"event":"report.daily_summary","id":"daily_summary:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:2026-09-18","merchant_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","timestamp":"2026-09-19T21:35:23+07:00"}#การตรวจสอบลายเซ็น
คำนวณ hex(HMAC-SHA256(secret_key, X-Webhook-Timestamp + "|" + raw_body)) แล้วเทียบกับ X-Webhook-Signature ด้วยวิธีเปรียบเทียบที่ใช้เวลาคงที่ (constant time)
- ให้ใช้ byte ของ body ดิบ ไม่ใช่ JSON ที่แปลงกลับเป็นข้อความใหม่
- ปฏิเสธ timestamp ที่เก่าเกินกว่านโยบายของคุณ
import hmac, hashlib
def verify(secret_key: str, ts: str, raw_body: bytes, signature: str) -> bool:
mac = hmac.new(secret_key.encode(), ts.encode() + b"|" + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, signature)#แนวทางการกระทบยอด
- ต่อยอดวันต่อวัน ค่า
opening_balanceของแต่ละวันต้องเท่ากับclosing_balanceของวันก่อนหน้า ถ้าไม่ต่อกันแปลว่ามีวันที่ขาดหายไป ให้ดึงวันนั้นด้วยGET /v1/merchant-reports/daily-summary?date= - กู้คืน webhook ที่ตกหล่น หาก webhook ของเมื่อวานยังไม่มาถึงภายในประมาณ 01:00 น. ตามเวลากรุงเทพฯ ให้ดึงข้อมูลวันนั้นผ่าน API ตัวเลขจาก API และจาก webhook เป็นชุดเดียวกัน ให้จัดเก็บโดยอ้างอิง
dateแล้วเขียนทับ webhook ที่มาถึงช้าของวันเดียวกันจึงไม่ก่อปัญหา - กระทบยอดทีละรายการ ใช้
statement/entriesสำหรับวันเดียว หรือใช้transactionsและrefundsเทียบกับเลขที่คำสั่งซื้อของคุณ (order_id/reference) - ค่าธรรมเนียม จำนวนเงินทั้งหมดหักค่าธรรมเนียมแล้ว อย่าหักซ้ำอีก
หน้านี้แปลจากเอกสารต้นฉบับภาษาอังกฤษ ส่วนที่เป็นค่าทางเทคนิค — path, ชื่อฟิลด์, header, รหัสข้อผิดพลาด และชื่อคอลัมน์ในไฟล์ส่งออก — คงไว้เป็นภาษาอังกฤษตามที่ระบบใช้จริง

