PayMeNow

Merchant Reports API

The ERP reports API: transactions, the daily balance statement, refunds and the report.daily_summary webhook.

Generated from merchant-reports-api.md

Machine access to the reports of the merchant dashboard: pay-in history, the daily balance statement, one day's movements, refunds, and the daily summary. It uses the same API key and HMAC signature as the payment API, so an ERP never needs a dashboard login.

A companion webhook, report.daily_summary, pushes each finished day's summary to your webhook URL.

No fee figures. No response, file or webhook contains a fee, fee rate, MDR or gross/net split. Every amount is the net effect on your balance.

#Hosts

EnvironmentBase URLData
Productionhttps://api.<domain>/v1/merchant-reports/production data
Sandboxhttps://sandbox-api.<domain>/v1/merchant-reports/sandbox data

Your API key decides the merchant and the data set. A merchant_id in the query string is ignored.

#Authentication

Every request carries the same three headers as the payment API:

HeaderValue
X-API-Keyyour API key
X-TimestampUnix time in seconds; it must be within 5 minutes of the server clock
X-Signaturelowercase hex of HMAC-SHA256(secret_key, METHOD + "|" + PATH + "|" + X-Timestamp)
  • PATH is the URL path only, without the host and without the query string: /v1/merchant-reports/statement.
  • A GET has no body, so there is no trailing |. With a body, the payment API appends "|" + body.
  • Your whitelisted IPs apply here too.

Example. Here is a signed request for GET /v1/merchant-reports/daily-summary?date=2026-09-18 at X-Timestamp: 1789776900 with the 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"

Authentication errors. These return 401: MISSING_API_KEY, MISSING_SIGNATURE, MISSING_TIMESTAMP, INVALID_TIMESTAMP, TIMESTAMP_EXPIRED, INVALID_API_KEY and INVALID_SIGNATURE. These return 403: MERCHANT_INACTIVE and IP_NOT_WHITELISTED.

#Rate limit

Each merchant gets 60 requests per minute on /v1/merchant-reports/*. This bucket is separate from the payment API, so pulling reports never slows your payments.

When you go over the limit you get 429 RATE_LIMIT_EXCEEDED with Retry-After. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

#Conventions

  • Dates: from, to and date are YYYY-MM-DD calendar days in Asia/Bangkok. Ranges include both ends and can span at most 366 days.
  • Times: JSON timestamps are RFC 3339 in Asia/Bangkok, for example 2026-09-19T15:05:00+07:00. They are ordinary ISO-8601 instants — any standards-compliant parser reads them unchanged; only the offset differs from the Z form used before 2026-09-20.
  • Money: JSON numbers with 2 decimals, in THB.
  • Paging: page defaults to 1. page_size defaults to 50 and is capped at 200.
  • format:
    • json is the default.
    • csv is UTF-8 with a BOM. Times are Bangkok YYYY-MM-DD HH:MM:SS.
    • xlsx is an Excel workbook. Money cells are numeric (#,##0.00). Times are real Bangkok date-time cells, and dates are date cells.
    • CSV and XLSX contain every matching row, not one page. More than 100,000 rows returns 413 TOO_MANY_ROWS, so narrow the range. Files come back with Content-Disposition: attachment; filename="<kind>_<from>_<to>.<ext>".
  • Errors: {"code": "...", "message": "..."}. The codes are INVALID_RANGE, INVALID_PARAM, TOO_MANY_ROWS, the authentication codes above, and UPSTREAM_UNAVAILABLE (502, safe to retry).

#Endpoints

#GET /v1/merchant-reports/daily-summary?date=YYYY-MM-DD

This endpoint returns one finished Bangkok day. date must be before today in Bangkok, otherwise you get 400 INVALID_RANGE.

The six kinds are always present. Their amounts are signed net balance effects: pay-ins are positive, and payouts and settlements are negative. They always reconcile: Σ kinds.amount = closing_balance − opening_balance, and 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"
}

A day without movements returns count 0 and amount 0 for every kind. On that day the opening and closing balances are both the last balance before the day.

format=csv|xlsx returns one row, named daily_summary_<date>.csv or daily_summary_<date>.xlsx. The columns are:

  • Date, Opening balance, Money in, Money out
  • a count column and an amount column for each of: 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]

This endpoint returns pay-ins created in the range, newest first.

  • status is one of pending, processing, pending_approval, completed, failed, expired, refunded, cancelled.
  • q matches a transaction id prefix, or part of your order id, the external reference or the payer name.
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 covers the range and q but ignores status.

The CSV and XLSX columns are: 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]

This endpoint returns the daily balance statement: one row per Bangkok day that has movements, oldest first.

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 is positive here, while in the daily summary the settlement amount is signed.

The CSV and XLSX columns are: Date, Opening balance, Money in, Money out, Settlements (count), Settlements (amount), Closing balance.

#GET /v1/merchant-reports/statement/entries?date[&format]

This endpoint returns one day's movements, oldest first. The kind values are payin, payout, settlement, withdraw_returned, payin_reversed and adjustment. reference is your order id for a pay-in or payout, the WDR… number for a settlement, and null otherwise.

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" }]
}

The CSV and XLSX columns are: Time, Type, Reference, Transaction ID, Amount, Balance after. The XLSX file is named statement_entries_<date>.xlsx, and the CSV keeps statement_entries_<date>_<date>.csv.

#GET /v1/merchant-reports/refunds?from&to[&q&page&page_size&format]

This endpoint returns refunded pay-ins whose refund falls in the range, newest refund first.

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 }
}

The CSV and XLSX columns are: Refunded at, Transaction ID, Order ID, Amount, Reason, Paid at.

#Webhook report.daily_summary

#Subscribing

Subscribe in the merchant dashboard under Settings › API, which needs a configured webhook URL. You can also add report.daily_summary to the events of your webhook configuration. The test button re-sends a chosen day at once, at most 5 times per minute. A test event's id ends in :test:<unix>.

#When it is sent

The summary is sent once per Bangkok day, for the previous day, at about 00:15 Asia/Bangkok. It is sent even when the day had no movements, so "no activity" is distinguishable from "not delivered".

If a send was missed, for example during maintenance, any of the last 3 days that was not sent yet goes out on the next run. The data set is your merchant's current environment.

#Delivery

Delivery uses the same mechanism as the transaction webhooks: an HTTPS POST to your webhook URL, signed with your API secret key.

  • A delivery succeeds when you answer with any 2xx within 10 seconds.
  • A failed delivery is retried every minute, up to 5 attempts.
  • The same day never produces a second delivery.
  • Use id (daily_summary:<merchant_id>:<date>) as your idempotency key.

#Headers

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 is the delivery key, a UUIDv5 of id. It is the same on every attempt.
  • timestamp in the body is the time of the attempt, so it changes on each retry.

#Body

data is exactly the daily-summary response. Keys arrive in alphabetical order, and numbers arrive with 2 decimals.

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"}

#Verifying the signature

Compute hex(HMAC-SHA256(secret_key, X-Webhook-Timestamp + "|" + raw_body)) and compare it with X-Webhook-Signature in constant time.

  • Use the raw body bytes, not re-serialised JSON.
  • Reject timestamps that are too old for your policy.
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)

#Reconciliation guidance

  • Chain the days. Each day's opening_balance must equal the previous day's closing_balance. A gap means a day is missing: fetch it with GET /v1/merchant-reports/daily-summary?date=.
  • Recover missed webhooks. If a webhook for yesterday has not arrived by about 01:00 Bangkok, pull the day through the API. The API and the webhook return the same numbers. Store by date and overwrite, so a late webhook for the same day is harmless.
  • Match line by line. Use statement/entries for one day, or transactions and refunds with your order ids (order_id / reference).
  • Fees. Amounts are already net of fees. Do not subtract fees again.