PayMeNow

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)

EnvironmentBase URLข้อมูล
Productionhttps://api.<domain>/v1/merchant-reports/ข้อมูลจริง
Sandboxhttps://sandbox-api.<domain>/v1/merchant-reports/ข้อมูลทดสอบ

API key เป็นตัวกำหนดว่าเป็นร้านค้าใดและใช้ข้อมูลชุดไหน หากส่ง merchant_id มาใน query string ระบบจะไม่สนใจค่านั้น

#การยืนยันตัวตน (Authentication)

ทุกคำขอต้องแนบ header สามตัวเดียวกับ API รับ-จ่ายเงิน:

Headerค่า
X-API-KeyAPI 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:

text
string to sign : GET|/v1/merchant-reports/daily-summary|1789776900
X-Signature    : b80b3ebf9bd996732d1cd7b65805446ceb57f775b4bed4b9c24ccf35fd61bfee
bash
TS=$(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

json
{
  "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, cancelled
  • q ใช้ค้นจากส่วนต้นของรหัสรายการ หรือบางส่วนของเลขที่คำสั่งซื้อ เลขอ้างอิงภายนอก หรือชื่อผู้ชำระเงิน
json
{
  "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]

คืนรายงานยอดคงเหลือรายวัน หนึ่งแถวต่อหนึ่งวันตามเวลากรุงเทพฯ ที่มีรายการเคลื่อนไหว เรียงจากเก่าไปใหม่

json
{
  "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 ในกรณีอื่น

json
{
  "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]

คืนรายการรับเงินที่ถูกคืนเงิน โดยนับจากวันที่คืนเงินอยู่ในช่วงที่ระบุ เรียงจากรายการที่คืนเงินล่าสุดก่อน

json
{
  "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>) เป็นกุญแจกันการประมวลผลซ้ำ
text
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-27d373a55d35
  • X-Transaction-ID คือกุญแจของการนำส่ง เป็น UUIDv5 ที่สร้างจาก id และมีค่าเท่าเดิมทุกครั้งที่ลองส่งใหม่
  • timestamp ใน body คือเวลาของการส่งครั้งนั้น จึงเปลี่ยนไปในการส่งซ้ำแต่ละครั้ง

#Body

data คือ response ของสรุปรายวันทุกประการ คีย์เรียงตามตัวอักษร และตัวเลขมีทศนิยม 2 ตำแหน่ง

json
{"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 ที่เก่าเกินกว่านโยบายของคุณ
python
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, รหัสข้อผิดพลาด และชื่อคอลัมน์ในไฟล์ส่งออก — คงไว้เป็นภาษาอังกฤษตามที่ระบบใช้จริง