Invoices
Search invoices across applications, drill into a single bill, and run on-demand overdue-reminder sweeps from the operations dashboard.
Operators with invoice:read can search and inspect invoices across every
application their grants cover, and operators with
app:manage can trigger an overdue-reminder (dunning) sweep on demand. These are
the cross-app, session-authenticated counterparts to the app-API-key
invoicing endpoints — same invoices, wider lens.
All endpoints below live under /admin/invoices and require an admin session:
-H "Authorization: Bearer SESSION_TOKEN"Every response is wrapped in the standard envelope
({ "data": …, "meta": …, "pagination": … }); the examples below show the
data payload and, where relevant, the pagination block. See
responses and errors for the full shape.
Reads are automatically narrowed to the apps your grants cover
(app isolation). An invoice owned by an app
outside your scope is invisible — a direct lookup of it reads back as 404,
exactly like a missing id, so the dashboard never leaks another app's data.
Search invoices
GET /admin/invoices (requires invoice:read) returns a
paginated list of
InvoiceSummaryDto rows across the apps you are scoped to. Every filter is
optional; a null leaves that dimension unconstrained.
| Field | Type | Required | Notes |
|---|---|---|---|
page | integer | No | 0-based page index. Default 0. |
size | integer | No | Page size. Default 20. |
sortBy | string | No | Sort property. Default createdAt. |
order | enum | No | ASC or DESC. Default DESC. |
paginate | boolean | No | false returns the full unpaged set. Default true. |
applicationId | long | No | A specific app within your scope. |
mode | enum | No | TEST or LIVE. Omit for both. |
status | enum | No | Exact invoice status (see below). Case-sensitive enum name. |
number | string | No | Case-insensitive substring match on the invoice number. |
customerRefId | long | No | The app's internal customer reference id. |
from | date (ISO YYYY-MM-DD) | No | Created on/after this day (inclusive). |
to | date (ISO YYYY-MM-DD) | No | Created on/before this day (inclusive). |
The from/to window filters on the invoice creation timestamp, and to
is inclusive (the service advances it to the end of that day internally).
Invoice status values
status accepts one of the persisted lifecycle states:
DRAFT, ISSUED, PARTIALLY_PAID, PAID, VOID, OVERDUE. OVERDUE is set
by the overdue sweep when an unpaid invoice's due date passes — it is a persisted
status, never recomputed on read.
Example
curl "http://localhost:8080/api/v1/admin/invoices?applicationId=42&status=OVERDUE&size=20" \
-H "Authorization: Bearer SESSION_TOKEN"{
"data": [
{
"id": 90871,
"applicationId": 42,
"applicationName": "Acme Storefront",
"number": "INV-2026-000314",
"mode": "LIVE",
"status": "OVERDUE",
"customerRefId": 5521,
"customerExternalId": "cust_acme_8842",
"customerName": "Rahim Uddin",
"customerEmail": "[email protected]",
"customerPhone": "+8801700000000",
"totalMinor": 1500000,
"amountPaidMinor": 0,
"amountDueMinor": 1500000,
"currency": "BDT",
"dueDate": "2026-05-31",
"issuedAt": "2026-05-01T09:12:00Z",
"createdAt": "2026-05-01T09:12:00Z",
"updatedAt": "2026-06-01T00:05:00Z"
}
],
"meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
"pagination": { "page": 0, "size": 20, "totalElements": 7, "totalPages": 1 }
}All monetary fields are integer minor units (paisa): 1500000 is
1,500.00 BDT. Currency is BDT-only in v1. See money.
InvoiceSummaryDto fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | long | Yes | Invoice id. |
applicationId | long | Yes | Owning app id — every row carries it so you can group/filter by app. |
applicationName | string | No | Resolved display name; omitted when the owning app can no longer be resolved (fall back to the id). |
number | string | Yes | Per-app invoice number. |
mode | string | Yes | TEST or LIVE. |
status | string | Yes | One of the lifecycle states. |
customerRefId | long | No | The app's internal customer reference id. |
customerExternalId | string | No | The app's own external customer id. |
customerName | string | No | Captured customer name. |
customerEmail | string | No | Captured customer email. |
customerPhone | string | No | Captured customer phone. |
totalMinor | long | Yes | Invoice total, minor units. |
amountPaidMinor | long | Yes | Amount settled so far, minor units. |
amountDueMinor | long | Yes | Outstanding balance (total − paid), minor units. |
currency | string | Yes | Always BDT in v1. |
dueDate | date | No | Payment due date. |
issuedAt | datetime | No | When the invoice was issued. |
createdAt | datetime | Yes | Creation timestamp. |
updatedAt | datetime | Yes | Last update timestamp. |
Invoice detail
GET /admin/invoices/{id} (requires invoice:read) returns one invoice's full
detail as an InvoiceDto — its line items (INV-5) and the payment(s) settling it
(INV-6). The result is app-scope-honoured: an invoice owned by an app outside
your scope, or an unknown id, both return 404.
curl "http://localhost:8080/api/v1/admin/invoices/90871" \
-H "Authorization: Bearer SESSION_TOKEN"{
"data": {
"id": 90871,
"number": "INV-2026-000314",
"status": "OVERDUE",
"mode": "LIVE",
"applicationId": 42,
"applicationName": "Acme Storefront",
"subscriptionId": null,
"priceId": null,
"customerReference": "cust_acme_8842",
"customerName": "Rahim Uddin",
"customerEmail": "[email protected]",
"customerPhone": "+8801700000000",
"totalMinor": 1500000,
"amountPaidMinor": 0,
"amountDueMinor": 1500000,
"currency": "BDT",
"dueDate": "2026-05-31",
"issuedAt": "2026-05-01T09:12:00Z",
"lineItems": [
{ "id": 1, "description": "Annual plan", "qty": 1, "unitAmountMinor": 1500000, "lineTotalMinor": 1500000 }
],
"payments": [],
"metadata": { "po": "PO-7781", "team": "finance" },
"createdAt": "2026-05-01T09:12:00Z",
"updatedAt": "2026-06-01T00:05:00Z"
},
"meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
"pagination": null
}InvoiceDto fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | long | Yes | Invoice id. |
number | string | Yes | Per-app invoice number. |
status | string | Yes | Lifecycle state. |
mode | string | Yes | TEST or LIVE. |
applicationId | long | Yes | Owning app id. |
applicationName | string | No | Resolved by the admin detail view so a deep link renders standalone; omitted when unresolved. |
subscriptionId | long | No | The subscription this invoice renews (B6); null for an ad-hoc invoice. |
priceId | long | No | The recurring price the cycle amount came from; null for an ad-hoc invoice. |
customerReference | string | No | The app's external customer id this invoice was billed to. |
customerName | string | No | Captured customer contact; omitted when unresolved. |
customerEmail | string | No | Captured customer email; omitted when unresolved. |
customerPhone | string | No | Captured customer phone; omitted when unresolved. |
totalMinor | long | Yes | Invoice total, minor units. |
amountPaidMinor | long | Yes | Amount settled so far, minor units. |
amountDueMinor | long | Yes | Outstanding balance (total − paid), minor units. |
currency | string | Yes | Always BDT in v1. |
dueDate | date | No | Payment due date. |
issuedAt | datetime | No | When the invoice was issued. |
lineItems | array | Yes | The invoice's line items (see below). |
payments | array | Yes | The payment(s) linked to this invoice (INV-6); empty until one is raised. |
payableUrl | string | No | Hosted-checkout link routing into payment, when minted. |
metadata | JSON object | No | Client-supplied metadata captured at create time, echoed back as a nested JSON object (not a string); omitted when none. |
createdAt | datetime | Yes | Creation timestamp. |
updatedAt | datetime | Yes | Last update timestamp. |
metadata is the optional JSON object stored as-is and echoed back unchanged on
the settlement webhook; it is never interpreted or merged.
InvoiceLineItemDto fields
| Field | Type | Required | Notes |
|---|---|---|---|
id | long | Yes | Line item id. |
description | string | Yes | Line description. |
qty | integer | Yes | Quantity. |
unitAmountMinor | long | Yes | Unit price, minor units. |
lineTotalMinor | long | Yes | Line total (unitAmountMinor × qty), minor units. |
The payments array carries the same PaymentDto shape documented under
transactions. For refund operations on a settling
payment, see refund approval.
On-demand overdue-reminder sweep
POST /admin/invoices/reminders (requires app:manage, not invoice:*)
runs the dunning sweep immediately and returns the number of reminders
actually sent as a plain integer. This is the same sweep the scheduled
DunningScheduler runs — exposed as a manual trigger for operators.
The reminders endpoint is gated by app:manage, not by an invoice:*
permission. Sending customer-facing dunning email is an app-management action,
so an operator who can only read invoices cannot trigger a sweep.
The sweep loads every OVERDUE invoice across all apps, then dispatches a
reminder for each one only when two further gates pass: the owning app has
dunning enabled, and the customer left an email. Each gate is a quiet no-op (not
an error), and a failure on one invoice is logged and swallowed so a single bad
row never aborts the whole run.
curl -X POST "http://localhost:8080/api/v1/admin/invoices/reminders" \
-H "Authorization: Bearer SESSION_TOKEN"{
"data": 4,
"meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
"pagination": null
}Here 4 overdue invoices belonged to dunning-enabled apps and had a customer
email; the rest were skipped.
Per-app dunning switch
Which apps get swept is governed by each app's per-app dunningEnabled switch,
which is off by default. Turn it on through the app's settings:
curl -X PATCH "http://localhost:8080/api/v1/admin/apps/42/settings" \
-H "Authorization: Bearer SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "dunningEnabled": true }'An overdue invoice belonging to an app with dunningEnabled = false is silently
skipped by the sweep. See apps for the full settings payload.
Dashboard aggregates
Two read-only aggregates feed the per-app invoice dashboard. Both require
invoice:read and both take applicationId and mode as required query
parameters — because an admin session is not bound to a single app or mode, the
operator selects them explicitly.
Picking mode as a query parameter is the dashboard-aggregate exception to the
usual rule that the mode is derived from the credential,
never from caller input — that rule governs app-API-key requests. See
dashboard analytics for the full treatment.
If you are outside your scope for the requested app, both endpoints read back
zeroed rather than erroring.
Metrics
GET /admin/invoices/metrics?applicationId={id}&mode={TEST|LIVE} returns an
InvoiceMetricsDto. Optional from/to (ISO dates) bound a [from, to) window
on invoice creation; omit them for all-time.
| Field | Type | Required | Notes |
|---|---|---|---|
totalCount | long | Yes | Invoices created in the window. Always a concrete 0, never null. |
paidCount | long | Yes | How many of those are PAID. |
overdueCount | long | Yes | How many are OVERDUE — read from the persisted status (set by the overdue sweep), never recomputed from due_date. |
outstandingMinor | long | Yes | Gross outstanding (total − paid) summed over every invoice that is neither PAID nor VOID — a voided bill carries no balance. |
curl "http://localhost:8080/api/v1/admin/invoices/metrics?applicationId=42&mode=LIVE" \
-H "Authorization: Bearer SESSION_TOKEN"{
"data": {
"totalCount": 128,
"paidCount": 96,
"overdueCount": 7,
"outstandingMinor": 5550000
},
"meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
"pagination": null
}Aging
GET /admin/invoices/aging?applicationId={id}&mode={TEST|LIVE} returns a list of
four InvoiceAgingBucketDto bands. Optional asOf (ISO date, default today)
sets the snapshot day.
Aging is an as-of snapshot, not a [from, to) window: a non-PAID/non-VOID
invoice with a due_date on or before asOf falls into a band by how many days
its due date precedes the snapshot. Invoices not yet due (or with no due date)
never appear. The four bands are always present (zeroed when empty) so the
chart renders a stable axis.
| Field | Type | Required | Notes |
|---|---|---|---|
bucket | string | Yes | Band label: 0-30, 31-60, 61-90, or 90+. |
count | long | Yes | Count of unpaid invoices in this band. |
outstandingMinor | long | Yes | Gross outstanding (total − paid) in this band, minor units. |
curl "http://localhost:8080/api/v1/admin/invoices/aging?applicationId=42&mode=LIVE&asOf=2026-06-30" \
-H "Authorization: Bearer SESSION_TOKEN"{
"data": [
{ "bucket": "0-30", "count": 12, "outstandingMinor": 4500000 },
{ "bucket": "31-60", "count": 3, "outstandingMinor": 900000 },
{ "bucket": "61-90", "count": 1, "outstandingMinor": 150000 },
{ "bucket": "90+", "count": 0, "outstandingMinor": 0 }
],
"meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
"pagination": null
}Related
Invoicing guide
The app-API-key endpoints that create and read invoices.
Payable invoice page
Mint a hosted-checkout link for an invoice and the public pay page.
Apps
Toggle the per-app dunning switch and other settings.
Dashboard analytics
The full set of per-app, mode-aware dashboard aggregates.
Transactions
The settling payments shown on an invoice's detail.
RBAC
How invoice:read and app:manage grants scope what you can see and do.
Subscriptions (operations)
Drive the subscription billing sweeps, search and inspect subscriptions across applications, and cancel, pause, or resume them from the admin dashboard.
Dashboard analytics
Per-app, mode-aware metrics rows, time-series trends, invoice aging and revenue leaderboards for the App-Selector dashboard.