OI Payments Docs
Admin dashboard

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.

FieldTypeRequiredNotes
pageintegerNo0-based page index. Default 0.
sizeintegerNoPage size. Default 20.
sortBystringNoSort property. Default createdAt.
orderenumNoASC or DESC. Default DESC.
paginatebooleanNofalse returns the full unpaged set. Default true.
applicationIdlongNoA specific app within your scope.
modeenumNoTEST or LIVE. Omit for both.
statusenumNoExact invoice status (see below). Case-sensitive enum name.
numberstringNoCase-insensitive substring match on the invoice number.
customerRefIdlongNoThe app's internal customer reference id.
fromdate (ISO YYYY-MM-DD)NoCreated on/after this day (inclusive).
todate (ISO YYYY-MM-DD)NoCreated 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

FieldTypeRequiredNotes
idlongYesInvoice id.
applicationIdlongYesOwning app id — every row carries it so you can group/filter by app.
applicationNamestringNoResolved display name; omitted when the owning app can no longer be resolved (fall back to the id).
numberstringYesPer-app invoice number.
modestringYesTEST or LIVE.
statusstringYesOne of the lifecycle states.
customerRefIdlongNoThe app's internal customer reference id.
customerExternalIdstringNoThe app's own external customer id.
customerNamestringNoCaptured customer name.
customerEmailstringNoCaptured customer email.
customerPhonestringNoCaptured customer phone.
totalMinorlongYesInvoice total, minor units.
amountPaidMinorlongYesAmount settled so far, minor units.
amountDueMinorlongYesOutstanding balance (total − paid), minor units.
currencystringYesAlways BDT in v1.
dueDatedateNoPayment due date.
issuedAtdatetimeNoWhen the invoice was issued.
createdAtdatetimeYesCreation timestamp.
updatedAtdatetimeYesLast 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

FieldTypeRequiredNotes
idlongYesInvoice id.
numberstringYesPer-app invoice number.
statusstringYesLifecycle state.
modestringYesTEST or LIVE.
applicationIdlongYesOwning app id.
applicationNamestringNoResolved by the admin detail view so a deep link renders standalone; omitted when unresolved.
subscriptionIdlongNoThe subscription this invoice renews (B6); null for an ad-hoc invoice.
priceIdlongNoThe recurring price the cycle amount came from; null for an ad-hoc invoice.
customerReferencestringNoThe app's external customer id this invoice was billed to.
customerNamestringNoCaptured customer contact; omitted when unresolved.
customerEmailstringNoCaptured customer email; omitted when unresolved.
customerPhonestringNoCaptured customer phone; omitted when unresolved.
totalMinorlongYesInvoice total, minor units.
amountPaidMinorlongYesAmount settled so far, minor units.
amountDueMinorlongYesOutstanding balance (total − paid), minor units.
currencystringYesAlways BDT in v1.
dueDatedateNoPayment due date.
issuedAtdatetimeNoWhen the invoice was issued.
lineItemsarrayYesThe invoice's line items (see below).
paymentsarrayYesThe payment(s) linked to this invoice (INV-6); empty until one is raised.
payableUrlstringNoHosted-checkout link routing into payment, when minted.
metadataJSON objectNoClient-supplied metadata captured at create time, echoed back as a nested JSON object (not a string); omitted when none.
createdAtdatetimeYesCreation timestamp.
updatedAtdatetimeYesLast 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

FieldTypeRequiredNotes
idlongYesLine item id.
descriptionstringYesLine description.
qtyintegerYesQuantity.
unitAmountMinorlongYesUnit price, minor units.
lineTotalMinorlongYesLine 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.

FieldTypeRequiredNotes
totalCountlongYesInvoices created in the window. Always a concrete 0, never null.
paidCountlongYesHow many of those are PAID.
overdueCountlongYesHow many are OVERDUE — read from the persisted status (set by the overdue sweep), never recomputed from due_date.
outstandingMinorlongYesGross 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.

FieldTypeRequiredNotes
bucketstringYesBand label: 0-30, 31-60, 61-90, or 90+.
countlongYesCount of unpaid invoices in this band.
outstandingMinorlongYesGross 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
}

On this page