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; aGETwould 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 whyinclude_paymentstakestrue,"true",1or"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_idis 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 parses1999.99into 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
-
pageis 1-based.limitdefaults to 50 and is clamped to 200 — ask for more and you get 200 with atotal_pagesyou can act on, rather than an error. A page past the end returns an emptydataarray. - 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.
| range | Covers |
|---|---|
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.
| Group | Per minute | Why |
|---|---|---|
| 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
| Field | Required | Notes |
|---|---|---|
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
| Field | Required | Notes |
|---|---|---|
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
| Field | Required | Notes |
|---|---|---|
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.
| Code | Name | Also 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.
| origin | Meaning |
|---|---|
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
fromafter itstois 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-Afteron 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.