PayFlow

Reporting API

Twelve read-only endpoints over your own payments and payouts, on the credential you already have. Same HMAC scheme as the payment API, one response shape for all twelve, and nothing here can move money.

The basics

Base URL
https://payflow.promoticsstudio.com
Method
POST, on every path, including the ones that only read. A signed request carries its parameters in the body; a GET would write your date ranges and your customers’ order references into every access log between you and us.
Content type
application/json. Form encoding is accepted too, which is why include_payments takes true, "true", 1 or "yes" equally.
Credentials
The same merchant id and API secret as the payment API, from your API keys page. This API calls the id merchant_mid; merchant_id is accepted as well, so a client you already have does not need renaming.
Timezone
Pakistan Standard Time, +05:00, for every window and every timestamp. “Today” means today here. Timestamps come back as ISO 8601 with the offset on them, so you never have to assume it.
Currency
PKR, and every amount is a string with exactly two decimal places. Strings rather than JSON numbers on purpose: a language that parses 1999.99 into a float has already lost a paisa somewhere.

Signing a request

Every request carries merchant_mid, timestamp (UNIX seconds) and signature. The signature is a lowercase hex HMAC-SHA256 of the signing string below, keyed with your API secret — the same computation as the payment API, over a shorter pre-image.

Three formulas cover all twelve endpoints. Which one a path uses is printed in its own section, and in the summary table further down.

Used by Signing string
Fixed-window and dashboard endpoints merchant_mid|timestamp
The two ranged reports merchant_mid|range|timestamp
The two lookups merchant_mid|identifier|timestamp

What is not signed

status, issuer, origin, page and limit are all outside the formula, deliberately. They only ever narrow a result set the signature already authorises in full, so covering them would buy nothing — and it would mean a fresh signature for every page, which makes walking a year of history absurd.

The range slot

For a shorthand, sign the shorthand: last_7_days, not the dates it resolves to. For a custom window, sign the two dates joined by a hyphen — 2026-07-01-2026-07-23 — and not the word custom. Both worked examples are below.

The clock

A timestamp more than 10 minutes from ours is refused with PGW_400_TIMESTAMP before anything else is checked. Run NTP. This is the single most common cause of a signature that looks correct and is rejected anyway.

Worked examples

All four use the secret pgw_sk_example0000000000000000000000000000000000000000, which is obviously not a real one, and the fixed past timestamp 1757000000 so the digests below never change. Reproduce any of them and your implementation matches ours.

A ranged report, shorthand window

signing string  PGW-M-000123|last_7_days|1757000000
signature       76bc4e54b86e7db7c711c2b2f229163b090a4e431fc11c0ab639af895c66c64e

A ranged report, custom window

signing string  PGW-M-000123|2026-07-01-2026-07-23|1757000000
signature       a2653151b66fdc82322be86e3778f3beda6a74f7e4e511e2be8264e8afc7e7a3

Note the four hyphens: two inside each date, one joining them. Send from and to in the body as separate fields.

A lookup

signing string  PGW-M-000123|PGW-20250904-9f3a2c1b|1757000000
signature       fd6c788fa17d0d8e5c5e0c133ca375d813b41e677ff0558d36f3ea58d18da23e

A fixed-window endpoint

signing string  PGW-M-000123|1757000000
signature       f00fdb2ac48eb83a5d023bfab237554c7593e2053f9daa02a3f7720d223fe162

The envelope

Every successful response, on every path, has the same top-level keys. A lookup that returns one record still carries pagination; a report with no rows still carries a summary of zeros. Write one parser.

{
  "success": true,
  "request_id": "a3f19c02be7d4416",
  "server_time": "2025-09-04T20:33:20+05:00",
  "merchant_mid": "PGW-M-000123",
  "range": { "range": "last_7_days", "from": "2026-09-04", "to": "2026-09-10", "days": 7 },
  "pagination": { "page": 1, "limit": 50, "total_records": 128, "total_pages": 3 },
  "summary": { },
  "data": [ ]
}
request_id
Unique to the response, not to the request. Quote it in a support email and we can find the exact call in our logs.
summary
Aggregates the whole window, not the page you are on. A summary that described the 50 rows in front of you would be a subtotal pretending to be a total.
pagination
page is 1-based. limit defaults to 50 and is clamped to 200 — ask for more and you get 200 with a total_pages you can act on, rather than an error. A page past the end returns an empty data array.
data
Always an array, even for a lookup, and always present.

Errors use the same success flag and drop the rest — see Errors.

Date ranges

The two ranged reports take either a range shorthand or an explicit from and to pair of YYYY-MM-DD dates. Supply dates without a range and custom is assumed. Supply neither and you get last_7_days.

rangeCovers
today Midnight this morning until now.
yesterday The whole of the previous day.
this_week Monday this week until now.
last_week The whole of the previous Monday-to-Sunday week.
this_month The 1st of this month until now.
last_month The whole of the previous calendar month.
last_7_days The seven calendar days ending today.
last_30_days The thirty calendar days ending today.
custom The explicit from and to dates supplied with the request.

Both ends are inclusive. A window that runs up to the present ends at the current second rather than at tonight’s midnight, so a total never includes hours that have not happened yet. Rolling windows are anchored to midnight, not to the clock: last_7_days is the seven calendar days ending today, so running the same report twice five minutes apart gives you the same rows.

The widest window one call may cover is 366 days. Beyond that, PGW_400_RANGE — page through in shorter windows.

Rate limits

Per merchant, per minute, in fixed windows. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 also carries Retry-After, in seconds, which is exactly how long until the current window ends.

GroupPer minuteWhy
Reports and fixed windows 60 The most expensive to serve: a page of rows plus a summary over the whole window.
Lookups 120 Cheap, and the ones an integration calls in a loop.
Dashboard 30 Aggregate your whole history. Poll them on a timer, not on every page view.

Windows are fixed rather than sliding, so the counter resets on the minute. If you are throttled at :47, waiting the Retry-After the response gave you is enough — there is no benefit to backing off further.

Payments report

POST

/auth/payments-report/

Every payment in a window, paginated, with a summary of the whole window rather than of the page you are on.

Signing string

merchant_mid|range|timestamp
FieldRequiredNotes
range No One of the shorthands in Date ranges. Defaults to last_7_days. Whatever you send here is what you sign.
from, to No YYYY-MM-DD, both inclusive. Sending them without a range means custom.
status No One of the payment statuses. An unrecognised value is refused rather than ignored — a filter that silently did nothing would hand you a bigger answer than you asked for.
issuer No The wallet, by code or by name. See Field reference.
origin No api or dashboard — whether the payment came from your integration or was raised by hand in your dashboard.
page, limit No 1-based; 50 rows by default, 200 at most.

200 OK

{
  "success": true,
  "request_id": "a3f19c02be7d4416",
  "server_time": "2025-09-04T20:33:20+05:00",
  "merchant_mid": "PGW-M-000123",
  "range": { "range": "last_7_days", "from": "2026-09-04", "to": "2026-09-10", "days": 7 },
  "filters": { "status": null, "issuer": null, "origin": null },
  "pagination": { "page": 1, "limit": 50, "total_records": 2, "total_pages": 1 },
  "summary": {
    "total_count": 2,
    "total_amount": "3499.98",
    "completed_count": 1,
    "completed_amount": "1999.99",
    "fee_amount": "45.00",
    "net_amount": "1954.99",
    "currency": "PKR",
    "by_status": [ { "status": "completed", "count": 1, "amount": "1999.99" } ],
    "by_issuer": [ { "issuer": 1, "issuer_name": "JazzCash", "count": 1, "amount": "1999.99" } ]
  },
  "data": [
    {
      "payment_id": "PGW-20250904-9f3a2c1b",
      "web_id": "ORDER-4417",
      "amount": "1999.99",
      "currency": "PKR",
      "issuer": 1,
      "issuer_name": "JazzCash",
      "status": "completed",
      "status_code": 200,
      "status_description": "The payment was completed and funds were received.",
      "fee_amount": "45.00",
      "net_amount": "1954.99",
      "customer_email": "buyer@example.com",
      "customer_mobile": "0300*****67",
      "origin": "api",
      "origin_note": null,
      "settlement_status": "unsettled",
      "created_at": "2025-09-04T20:33:20+05:00",
      "completed_at": "2025-09-04T20:34:56+05:00"
    }
  ]
}

by_status lists every status, including the ones with no payments in the window — “there were no refunds” is a different statement from “we do not report refunds”.

Rate limit: 60 per minute.

Payments today

POST

/auth/payments-today/

Midnight this morning until now, in Pakistan Standard Time.

Signing string

merchant_mid|timestamp

The payments report over a fixed window. The status, issuer, origin, page and limit filters all still work; only the signing string differs, because the path already fixed the window.

Rate limit: 60 per minute.

Payments yesterday

POST

/auth/payments-yesterday/

The whole of the previous day.

Signing string

merchant_mid|timestamp

The payments report over a fixed window. The status, issuer, origin, page and limit filters all still work; only the signing string differs, because the path already fixed the window.

Rate limit: 60 per minute.

Payments this week

POST

/auth/payments-week/

The seven calendar days ending today.

Signing string

merchant_mid|timestamp

The payments report over a fixed window. The status, issuer, origin, page and limit filters all still work; only the signing string differs, because the path already fixed the window.

Rate limit: 60 per minute.

Payments this month

POST

/auth/payments-month/

The thirty calendar days ending today.

Signing string

merchant_mid|timestamp

The payments report over a fixed window. The status, issuer, origin, page and limit filters all still work; only the signing string differs, because the path already fixed the window.

Rate limit: 60 per minute.

Settlements report

POST

/auth/settlements-report/

Your payouts in a window — gross, the fee actually deducted, and the net actually paid. Optionally with the payments inside each one.

Signing string

merchant_mid|range|timestamp
FieldRequiredNotes
range, from, to No As for the payments report.
include_payments No Adds a payments array to each payout. The page size drops to 10 batches when this is on, because each one expands into its own transactions.
page, limit No As above.

200 OK

{
  "success": true,
  "range": { "range": "last_30_days", "from": "2026-08-12", "to": "2026-09-10", "days": 30 },
  "include_payments": false,
  "pagination": { "page": 1, "limit": 50, "total_records": 4, "total_pages": 1 },
  "summary": {
    "batch_count": 4,
    "transaction_count": 311,
    "gross_amount": "618400.00",
    "fee_deducted": "13914.00",
    "net_payout": "604486.00",
    "paid_count": 3,
    "paid_net_payout": "451200.00",
    "currency": "PKR"
  },
  "data": [
    {
      "batch_id": "PGW-S-202509-0041",
      "period_start": "2026-09-01",
      "period_end": "2026-09-07",
      "transaction_count": 84,
      "gross_amount": "167900.00",
      "fee_amount": "3777.75",
      "net_payout": "164122.25",
      "currency": "PKR",
      "status": "paid",
      "payout_reference": "IBFT-77401293",
      "paid_at": "2025-09-04T20:34:56+05:00",
      "created_at": "2025-09-04T20:33:20+05:00"
    }
  ]
}

gross_amount minus fee_amount equals net_payout exactly. All three are read from the payout itself rather than recomputed from a rate, so they agree with your payout advice to the paisa.

A payout is included when its period overlaps your window, not only when it sits wholly inside it. A weekly payout covering the 8th to the 14th appears in a report for the 1st to the 10th, because part of it is money you earned in the window you asked about.

Rate limit: 60 per minute.

Settlements today

POST

/auth/settlements-today/

Payouts whose period touches today.

Signing string

merchant_mid|timestamp

Rate limit: 60 per minute.

Settlements this week

POST

/auth/settlements-week/

Payouts whose period touches the last seven days.

Signing string

merchant_mid|timestamp

Rate limit: 60 per minute.

Look up a payment

POST

/auth/payment-lookup/

One payment, by our reference or by yours.

Signing string

merchant_mid|identifier|timestamp
FieldRequiredNotes
payment_id One of Our reference, e.g. PGW-20250904-9f3a2c1b.
web_id One of Your own order reference, e.g. ORDER-4417.

Exactly one, because the signature covers exactly one: send two and which of them was signed is ambiguous, so the call is refused rather than answered about whichever we picked.

Between them these two address every payment you have — ours if you stored it, yours if you did not. There is no third key, and a request that carries neither is answered with the pair by name.

Rate limit: 120 per minute.

Look up a settlement

POST

/auth/settlement-lookup/

One payout and every payment inside it.

Signing string

merchant_mid|identifier|timestamp

Takes batch_id, the reference from your settlements report or your statement. The response is one payout with a payments array of every transaction inside it.

A batch id that does not exist and one belonging to another merchant answer the same PGW_404_NOT_FOUND. Telling those two apart would make this endpoint a way to discover other people’s references.

Rate limit: 120 per minute.

Merchant summary

POST

/auth/merchant-summary/

Today, the last seven days, the last thirty and lifetime, plus what is earned and not yet paid out.

Signing string

merchant_mid|timestamp

200 OK

{
  "success": true,
  "pagination": { "page": 1, "limit": 4, "total_records": 4, "total_pages": 1 },
  "summary": {
    "lifetime": { "count": 4187, "gross_amount": "8241900.00", "fee_amount": "185442.75", "net_amount": "8056457.25" },
    "unsettled": {
      "count": 62,
      "gross_amount": "121400.00",
      "net_amount": "118668.50",
      "eligible_count": 18,
      "eligible_net": "34220.00",
      "next_eligible_at": "2025-09-04T20:34:56+05:00"
    },
    "currency": "PKR"
  },
  "data": [
    { "period": "today", "from": "2026-09-10", "to": "2026-09-10", "count": 12, "gross_amount": "23400.00", "fee_amount": "526.50", "net_amount": "22873.50", "currency": "PKR" },
    { "period": "last_7_days", "from": "2026-09-04", "to": "2026-09-10", "count": 88, "gross_amount": "171200.00", "fee_amount": "3852.00", "net_amount": "167348.00", "currency": "PKR" },
    { "period": "last_30_days", "from": "2026-08-12", "to": "2026-09-10", "count": 366, "gross_amount": "712900.00", "fee_amount": "16040.25", "net_amount": "696859.75", "currency": "PKR" },
    { "period": "lifetime", "from": null, "to": null, "count": 4187, "gross_amount": "8241900.00", "fee_amount": "185442.75", "net_amount": "8056457.25", "currency": "PKR" }
  ]
}

Sales are counted by when a payment completed, not when it was created: one started on Monday and completed on Tuesday is Tuesday’s revenue, which is the day it will reconcile against. unsettled is what you have earned and not yet been paid — not a withdrawable balance, because part of it is still inside its clearing period.

Rate limit: 30 per minute.

Awaiting settlement

POST

/auth/payments-pending-settlement/

Completed payments not yet attached to a payout, each flagged with whether its clearing period has already elapsed.

Signing string

merchant_mid|timestamp

Ordered by when each payment becomes eligible rather than by when it completed, so the first row is the next money to move. Each row carries settlement_eligible_at and a boolean eligible_now: true means the clearing period has elapsed and it is waiting on the next settlement run rather than on the clock.

The summary is the same unsettled arithmetic the merchant summary reports, so the two endpoints cannot disagree.

Rate limit: 30 per minute.

Field reference

Issuers

issuer accepts either the code or the name, and is returned as the code with issuer_name beside it. The codes are permanent.

CodeNameAlso accepted
1 JazzCash jazzcash
2 EasyPaisa easypaisa
3 Scan to Pay qr

Statuses

The same vocabulary as the payment API, with the same numeric codes. Only completed payments are settled and paid out.

status status_code Meaning
created 100 Payment created, not yet charged.
awaiting_confirmation 101 Awaiting customer confirmation on their wallet.
completed 200 The payment was completed and funds were received.
failed 400 The payment attempt failed.
expired 102 The payment session expired before completion.
cancelled 103 The payment was cancelled.
refunded 106 The amount was returned to the customer.
abandoned 401 The customer left the payment unfinished.

Origins

Where a payment came from. Both kinds settle identically and carry the same fee — the label exists so you can tell your own integration’s traffic from requests your staff raised by hand.

originMeaning
api Created by the merchant's own integration calling the payments API.
dashboard Raised by hand from the merchant dashboard and sent to the customer as a link.

origin_note carries the description whoever raised a dashboard request typed for their own records, and is null for everything else.

Errors

A failed call answers with the HTTP status from the table below and this body. The success flag is the same one a successful response leads with, so you can branch on one field either way.

{
  "success": false,
  "status": "error",
  "code": "PGW_400_RANGE",
  "message": "The requested date range is invalid or exceeds the maximum window."
}
Code HTTP Message
PGW_400_VALIDATION 400 Required fields missing or invalid.
PGW_400_TIMESTAMP 400 Request timestamp is invalid or expired.
PGW_400_DECLINED 400 Payment could not be completed. Please try again.
PGW_400_RANGE 400 The requested date range is invalid or exceeds the maximum window.
PGW_401_AUTH 401 Invalid merchant credentials or signature.
PGW_403_INACTIVE 403 This merchant account is not active.
PGW_403_DOMAIN 403 Request origin domain is not authorized for this merchant.
PGW_404_NOT_FOUND 404 Transaction not found.
PGW_404_ENDPOINT 404 No such endpoint. Check the path and HTTP method.
PGW_405_METHOD 405 This endpoint does not accept that HTTP method.
PGW_409_DUPLICATE 409 This order reference has already been used.
PGW_409_STATE 409 This payment is not in a chargeable state.
PGW_409_IN_PROGRESS 409 A request for this payment is already being processed. Please wait a moment.
PGW_409_IDEMPOTENCY 409 This Idempotency-Key was already used for a different request.
PGW_410_EXPIRED 410 This payment session has expired. Please create a new one.
PGW_429_RATE_LIMIT 429 Too many requests. Please slow down and try again shortly.
PGW_500_INTERNAL 500 Something went wrong on our end. Please try again shortly.
PGW_503_UNAVAILABLE 503 Payment service is temporarily unavailable.

The ones this API sends most

PGW_400_RANGE
The window is backwards, unparseable, or wider than 366 days. A from after its to is the usual cause.
PGW_400_VALIDATION
A filter value outside its vocabulary, two lookup identifiers where one was expected, or none at all. The message names what was wrong and what is accepted.
PGW_401_AUTH
Almost always a signing string that differs from ours by one character. Check that you signed the shorthand rather than the dates it resolves to, then that you did not include the filters.
PGW_404_NOT_FOUND
No such record under your account. Every query on this API is scoped to you, so another merchant’s reference answers exactly as a nonexistent one does.
PGW_429_RATE_LIMIT
Over the per-minute budget for that group. Retry-After on the response is the exact number of seconds until the window resets.

Getting started

Your existing API secret already works here — there is nothing to enable and no second credential to manage. Verify your signing against the worked examples above, then call /auth/merchant-summary/: it needs no parameters beyond the three every request carries, so it is the shortest path to a first successful response.

Stuck on a signature you are sure is right? Send us the signing string you built, the timestamp and the request_id from a response — never your secret — at support@payflow.com, and we will tell you which character differs.