OI Payments Docs
Admin dashboard

Customer 360

Search customers across applications and open a single customer's profile with aggregated LIVE payment, invoice and refund activity.

Customer 360 is the operations-dashboard view of an app's customers: a cross-app search, a per-customer profile with aggregated activity, and two analytics reads (key metrics and a spend leaderboard) for the App-Selector dashboard.

It is distinct from the app-facing customer lookup, which is API-key-authenticated and confined to the calling app and mode. These endpoints are admin-session-authenticated and span every app the operator is scoped to.

Access

All four endpoints require an admin session and the customer:read authority. Send the session token as a bearer header:

-H "Authorization: Bearer SESSION_TOKEN"
EndpointPurposeAuthority
GET /admin/customersPaginated cross-app customer search.customer:read
GET /admin/customers/{id}One customer's full profile.customer:read
GET /admin/customers/metricsPer-app customer key metrics.customer:read
GET /admin/customers/topPer-app top customers by spend.customer:read

The coarse customer:read gate is checked on the controller. The fine-grained app-scope narrowing (RBAC-4) runs inside each use case off the acting admin. A scoped admin only ever sees customers of the apps their grants cover; an unrestricted admin sees every app. See app isolation and RBAC.

Every response uses the standard envelope; the rest of this page shows just the data payload (and pagination where it applies):

{
  "data": <payload|null>,
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": { "page": 0, "size": 20, "totalElements": 42, "totalPages": 3 }
}

Admin endpoints are authenticated by an admin session, not an app API key, so they carry no credential-derived mode. The mode-aware reads here (/metrics and /top) therefore take mode as an explicit query parameter — the same single exception to the credential-derived mode rule called out on dashboard analytics. The profile's money figures are not mode-parameterized: they are always LIVE-only (see below).

Search customers

GET /admin/customers returns a paginated list of customer summaries, narrowed to the apps your grants cover.

Query parameters

ParameterTypeRequiredDefaultNotes
querystringNoA single free-text term, matched as a case-insensitive substring against externalCustomerId, name, email or phone (OR). One search box finds a customer by any contact handle. A blank term is ignored.
applicationIdnumberNoRestrict to one app. Must be within your scope; outside it, you simply see no rows.
pagenumberNo0Zero-based page index.
sizenumberNo20Page size.
sortBystringNocreatedAtSort field.
orderstringNoDESCASC or DESC.
paginatebooleanNotrueSet false to return the full unpaged set.

CustomerSummaryDto

Each row projects the lightweight customer_ref pointer — no credential or secret material. Aggregated activity lives on the profile, not here.

FieldTypeNotes
idnumberInternal customer_ref id — the handle for the profile lookup below.
applicationIdnumberOwning app.
applicationNamestringResolved app display name.
externalCustomerIdstringThe app's own customer id.
namestringOptional display name (omitted when null).
emailstringOptional (omitted when null).
phonestringOptional (omitted when null).
curl "http://localhost:8080/api/v1/admin/customers?query=rahim&applicationId=42&size=20" \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": [
    {
      "id": 4815,
      "applicationId": 42,
      "applicationName": "Acme Storefront",
      "externalCustomerId": "cus_8f3a21",
      "name": "Rahim Uddin",
      "email": "[email protected]",
      "phone": "+8801712345678"
    }
  ],
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": { "page": 0, "size": 20, "totalElements": 1, "totalPages": 1 }
}

Customer profile

GET /admin/customers/{id} returns one customer's contact identity plus their aggregated LIVE payment, invoice and refund activity.

{id} is the internal customer_ref id (the id field from the search rows), not the app's externalCustomerId. If the customer's app is outside your scope, the response is 404 — the customer is treated as non-existent rather than leaking its presence.

The use case resolves the reference, enforces your customer:read app-scope, then fans out to the owning features' read interfaces. The refund leg bridges through the customer's payment ids, since refunds carry no customer foreign key.

All money figures on the profile are LIVE-only — test-mode activity is excluded — so totals only ever reflect real settlements. Amounts are integer minor units (paisa): 4500000 means 45,000.00 BDT.

CustomerProfileDto

FieldTypeNotes
idnumberInternal customer_ref id.
applicationIdnumberOwning app.
applicationNamestringResolved app display name.
externalCustomerIdstringThe app's own customer id.
namestringOptional display name (omitted when null).
emailstringOptional (omitted when null).
phonestringOptional (omitted when null).
paymentStatsobjectLIVE payment activity — see below.
invoiceStatsobjectLIVE invoice activity — see below.
refundStatsobjectSucceeded-refund activity — see below.

paymentStats — LIVE payments only.

FieldTypeNotes
countnumberAll LIVE payments for the customer.
succeededCountnumberLIVE payments that succeeded.
grossSucceededMinornumberGross succeeded amount in minor units.
successRatenumbersucceededCount / count as a 0..1 ratio; 0.0 when count is 0.
firstPaymentAtstringFirst LIVE payment timestamp; null when there are no LIVE payments.
lastPaymentAtstringLast LIVE payment timestamp; null when there are no LIVE payments.

invoiceStats — LIVE invoices only.

FieldTypeNotes
countnumberNumber of non-void invoices.
outstandingMinornumberSum of unpaid balances (total − paid) in minor units.

refundStats — across the customer's payments.

FieldTypeNotes
countnumberCount of SUCCEEDED refunds.
totalMinornumberGross refunded amount in minor units (SUCCEEDED only).
curl "http://localhost:8080/api/v1/admin/customers/4815" \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": {
    "id": 4815,
    "applicationId": 42,
    "applicationName": "Acme Storefront",
    "externalCustomerId": "cus_8f3a21",
    "name": "Rahim Uddin",
    "email": "[email protected]",
    "phone": "+8801712345678",
    "paymentStats": {
      "count": 12,
      "succeededCount": 9,
      "grossSucceededMinor": 4500000,
      "successRate": 0.75,
      "firstPaymentAt": "2026-01-14T09:32:10",
      "lastPaymentAt": "2026-06-21T18:04:55"
    },
    "invoiceStats": { "count": 7, "outstandingMinor": 150000 },
    "refundStats": { "count": 2, "totalMinor": 60000 }
  },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

An out-of-scope (or unknown) id returns a 404:

{
  "data": null,
  "meta": {
    "success": false,
    "message": "Customer not found with id: 4815",
    "errorCode": "RESOURCE_NOT_FOUND",
    "timestamp": "2026-06-30T12:00:00Z"
  },
  "pagination": null
}

Customer metrics

GET /admin/customers/metrics powers the dashboard metrics row: the total customers for an app and how many have at least one succeeded payment in the selected mode and window.

Query parameters

ParameterTypeRequiredNotes
applicationIdnumberYesThe app to report on.
modestringYesTEST or LIVE. Drives the mode-aware buyer count only.
fromstringNoWindow start, ISO yyyy-MM-dd (inclusive). Open when omitted.
tostringNoWindow end, ISO yyyy-MM-dd (inclusive — internally to + 1 day exclusive). Open when omitted.

CustomerMetricsDto

FieldTypeNotes
totalCountnumberTotal customer_ref rows for the app. Mode-agnostic — a customer row is shared across TEST and LIVE, so this ignores mode and the window.
withSucceededPaymentCountnumberDistinct customers with a succeeded payment in the selected mode and window. Mode-aware — the payment that proves a buyer is mode-scoped.

If you are not scoped to applicationId, both fields come back as concrete zeros ({ "totalCount": 0, "withSucceededPaymentCount": 0 }) — never null.

curl "http://localhost:8080/api/v1/admin/customers/metrics?applicationId=42&mode=LIVE&from=2026-06-01&to=2026-06-30" \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": { "totalCount": 1280, "withSucceededPaymentCount": 214 },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

Top customers

GET /admin/customers/top returns the per-app, mode-aware leaderboard of customers ranked by succeeded spend over the optional window.

Query parameters

ParameterTypeRequiredDefaultNotes
applicationIdnumberYesThe app to rank within.
modestringYesTEST or LIVE. Spend is computed in the selected mode.
fromstringNoWindow start, ISO yyyy-MM-dd (inclusive). Open when omitted.
tostringNoWindow end, ISO yyyy-MM-dd (inclusive). Open when omitted.
limitnumberNo10Top-N size, clamped to [1, 100].

TopCustomerRow

FieldTypeNotes
customerRefIdnumberInternal customer_ref id — pass to GET /admin/customers/{id} to drill in.
externalCustomerIdstringThe app's own customer id, for labelling.
applicationIdnumberOwning app.
grossSucceededMinornumberSucceeded spend in the window, in minor units. The ranking key (descending).
succeededCountnumberNumber of succeeded payments in the window.

If you are not scoped to applicationId, the result is an empty list.

curl "http://localhost:8080/api/v1/admin/customers/top?applicationId=42&mode=LIVE&limit=5" \
  -H "Authorization: Bearer SESSION_TOKEN"
{
  "data": [
    {
      "customerRefId": 4815,
      "externalCustomerId": "cus_8f3a21",
      "applicationId": 42,
      "grossSucceededMinor": 4500000,
      "succeededCount": 9
    },
    {
      "customerRefId": 5102,
      "externalCustomerId": "cus_a17b09",
      "applicationId": 42,
      "grossSucceededMinor": 3120000,
      "succeededCount": 6
    }
  ],
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:00:00Z" },
  "pagination": null
}

Date windows

from and to on /metrics and /top are ISO calendar dates (yyyy-MM-dd). The window is half-open internally — from maps to the start of that day and to maps to the start of the next day, so a to of 2026-06-30 includes every event on the 30th. Omit either bound to leave it open (wide sentinels are applied so the window is always a typed range).

On this page