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"| Endpoint | Purpose | Authority |
|---|---|---|
GET /admin/customers | Paginated cross-app customer search. | customer:read |
GET /admin/customers/{id} | One customer's full profile. | customer:read |
GET /admin/customers/metrics | Per-app customer key metrics. | customer:read |
GET /admin/customers/top | Per-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
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
query | string | No | — | A 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. |
applicationId | number | No | — | Restrict to one app. Must be within your scope; outside it, you simply see no rows. |
page | number | No | 0 | Zero-based page index. |
size | number | No | 20 | Page size. |
sortBy | string | No | createdAt | Sort field. |
order | string | No | DESC | ASC or DESC. |
paginate | boolean | No | true | Set 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.
| Field | Type | Notes |
|---|---|---|
id | number | Internal customer_ref id — the handle for the profile lookup below. |
applicationId | number | Owning app. |
applicationName | string | Resolved app display name. |
externalCustomerId | string | The app's own customer id. |
name | string | Optional display name (omitted when null). |
email | string | Optional (omitted when null). |
phone | string | Optional (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
| Field | Type | Notes |
|---|---|---|
id | number | Internal customer_ref id. |
applicationId | number | Owning app. |
applicationName | string | Resolved app display name. |
externalCustomerId | string | The app's own customer id. |
name | string | Optional display name (omitted when null). |
email | string | Optional (omitted when null). |
phone | string | Optional (omitted when null). |
paymentStats | object | LIVE payment activity — see below. |
invoiceStats | object | LIVE invoice activity — see below. |
refundStats | object | Succeeded-refund activity — see below. |
paymentStats — LIVE payments only.
| Field | Type | Notes |
|---|---|---|
count | number | All LIVE payments for the customer. |
succeededCount | number | LIVE payments that succeeded. |
grossSucceededMinor | number | Gross succeeded amount in minor units. |
successRate | number | succeededCount / count as a 0..1 ratio; 0.0 when count is 0. |
firstPaymentAt | string | First LIVE payment timestamp; null when there are no LIVE payments. |
lastPaymentAt | string | Last LIVE payment timestamp; null when there are no LIVE payments. |
invoiceStats — LIVE invoices only.
| Field | Type | Notes |
|---|---|---|
count | number | Number of non-void invoices. |
outstandingMinor | number | Sum of unpaid balances (total − paid) in minor units. |
refundStats — across the customer's payments.
| Field | Type | Notes |
|---|---|---|
count | number | Count of SUCCEEDED refunds. |
totalMinor | number | Gross 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
| Parameter | Type | Required | Notes |
|---|---|---|---|
applicationId | number | Yes | The app to report on. |
mode | string | Yes | TEST or LIVE. Drives the mode-aware buyer count only. |
from | string | No | Window start, ISO yyyy-MM-dd (inclusive). Open when omitted. |
to | string | No | Window end, ISO yyyy-MM-dd (inclusive — internally to + 1 day exclusive). Open when omitted. |
CustomerMetricsDto
| Field | Type | Notes |
|---|---|---|
totalCount | number | Total customer_ref rows for the app. Mode-agnostic — a customer row is shared across TEST and LIVE, so this ignores mode and the window. |
withSucceededPaymentCount | number | Distinct 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
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
applicationId | number | Yes | — | The app to rank within. |
mode | string | Yes | — | TEST or LIVE. Spend is computed in the selected mode. |
from | string | No | — | Window start, ISO yyyy-MM-dd (inclusive). Open when omitted. |
to | string | No | — | Window end, ISO yyyy-MM-dd (inclusive). Open when omitted. |
limit | number | No | 10 | Top-N size, clamped to [1, 100]. |
TopCustomerRow
| Field | Type | Notes |
|---|---|---|
customerRefId | number | Internal customer_ref id — pass to GET /admin/customers/{id} to drill in. |
externalCustomerId | string | The app's own customer id, for labelling. |
applicationId | number | Owning app. |
grossSucceededMinor | number | Succeeded spend in the window, in minor units. The ranking key (descending). |
succeededCount | number | Number 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).
Related
App isolation
Every record carries app_id + mode; one app never sees another.
Customers (concept)
What a customer reference is and how apps create one.
Look up customers (app API)
The API-key-authenticated, app-scoped customer lookup.
Dashboard analytics
The mode-as-query-parameter exception, explained in full.
Transactions
Drill from a customer into their individual payments.
RBAC
How customer:read grants and app scope are assigned.
Transactions
Search payments across every application you are scoped to, export the filtered set as CSV, and read per-app dashboard metrics and daily trends.
Operator refunds, approvals & refund analytics
Initiate refunds from the dashboard, approve above-threshold ones with four-eyes control, and read per-app refund metrics — all session-authenticated and app-scoped.