Reconciliation
Match gateway settlement to confirmed live payments, and review flagged mismatches.
Reconciliation matches the gateway's settlement records against the payments the service confirmed, surfacing any discrepancy for an operator to investigate.
Reconciliation is LIVE-only. Test-mode records move no real money and are excluded — see modes.
Both endpoints below are admin session-authenticated and gated by the
report:read authority. Reconciliation is a cross-app system view, so there is
no app-scope narrowing — a run spans every LIVE app at once and each result
carries its owning applicationId for attribution. See
RBAC for the authority model and
app isolation for the usual per-app rule this
view intentionally steps outside of.
All responses use the standard envelope:
{ "data": …, "meta": { "success": true, … }, "pagination": … }. The sections
below show only the data payload.
How a run classifies each item
A run matches the gateway settlement rows for a period against the internal LIVE
payments that settled in that period. The match is keyed on the
transactionRef (the payment reference echoed in the settlement report).
Settlement rows are indexed and consumed as internal payments match them, so
whatever is left over was present in settlement but not internally. An internal
payment with no settlement row that settled within a configurable timing
tolerance of the day's cutoff (default 6 hours) is DEFERRED — its settlement
may simply lag to the next cycle — otherwise it is MISSING_IN_SETTLEMENT.
Mismatches are never auto-adjusted. Every non-MATCHED outcome is purely a
flag for an operator to review and resolve out of band — the service does not
silently move money to make the numbers balance. The run can read internal
state and write result rows, but it structurally cannot touch the ledger,
payments or settlement records.
Reconciliation statuses
ReconciliationStatus has exactly five values. (There is no FLAGGED
status — "flagged" describes the four non-MATCHED outcomes collectively.)
| Status | Sides present | Meaning |
|---|---|---|
MATCHED | both | Settlement row and internal payment agree on reference and amount — nothing to do. |
AMOUNT_MISMATCH | both | Matched by reference but the amounts differ — flagged; figures are never silently adjusted. |
MISSING_IN_SETTLEMENT | internal only | An internal settled payment has no settlement row and settled too early to be explained by settlement lag — flagged. |
MISSING_INTERNAL | settlement only | A settlement row has no matching internal LIVE payment (e.g. present in the report but absent internally) — flagged. |
DEFERRED | internal only | Settled near the cutoff; within the timing tolerance it is expected to reconcile next cycle, so it is not flagged. |
Trigger a run — POST /admin/reconciliation/runs
Reconciles the already-imported settlement records for one period (the same sweep
the daily scheduler runs) and returns the run tally. period is a required
ISO date (yyyy-MM-dd) query parameter; gated by report:read. Re-running a
period is naturally idempotent — the run deletes that period's prior (derived)
results and recomputes them.
| Param | Type | Required | Notes |
|---|---|---|---|
period | date (ISO yyyy-MM-dd) | Yes | The cycle date to reconcile. LIVE-only; test-mode payments are never fetched. |
curl -X POST "http://localhost:8080/api/v1/admin/reconciliation/runs?period=2026-06-29" \
-H "Authorization: Bearer SESSION_TOKEN"The data payload is a ReconciliationRunSummary:
{
"data": {
"period": "2026-06-29",
"matched": 412,
"missingInSettlement": 1,
"missingInternal": 0,
"amountMismatch": 2,
"deferred": 7,
"ledgerBalanced": true
},
"meta": {
"success": true,
"message": null,
"errorCode": null,
"timestamp": "2026-06-30T02:00:00Z"
},
"pagination": null
}Run summary fields
| Field | Type | Required | Notes |
|---|---|---|---|
period | string (date) | Always | The cycle date reconciled. |
matched | integer | Always | Items where settlement and internal records agreed on reference and amount. |
missingInSettlement | integer | Always | Internal LIVE payments absent from settlement beyond the timing tolerance — flagged. |
missingInternal | integer | Always | Settlement rows with no internal payment — flagged. |
amountMismatch | integer | Always | Items matched by reference but disagreeing on amount — flagged. |
deferred | integer | Always | Near-cutoff internal payments expected to reconcile next cycle — not flagged. |
ledgerBalanced | boolean | Always | Whether the LIVE double-entry ledger summed to zero (the always-balance invariant). The run only verifies this — it never posts a correcting entry. |
Counts are minor-unit-free tallies, but amounts elsewhere (the result rows
below) are integer minor units (paisa): 150000 means 1,500.00 BDT, never
floating point. See money.
List results — GET /admin/reconciliation/results
Lists the per-item outcomes of the daily run — paginated and filterable by
mode, period, and status. Gated by report:read.
| Param | Type | Required | Notes |
|---|---|---|---|
page | integer | No | 0-based page index. Default 0. |
size | integer | No | Page size. Default 20. |
sortBy | string | No | Sort field. Default createdAt. |
order | ASC | DESC | No | Sort direction. Default DESC. |
paginate | boolean | No | Default true; set false to return the full unpaged list. |
mode | string | No | TEST or LIVE. Results are LIVE-only, so LIVE is the only mode with rows; an unrecognized value is rejected with 400. |
period | date (ISO yyyy-MM-dd) | No | Filter to a single cycle date, e.g. 2026-06-29. |
status | string | No | One of the five ReconciliationStatus values above; an unrecognized value is rejected with 400. |
curl "http://localhost:8080/api/v1/admin/reconciliation/results?period=2026-06-29&status=AMOUNT_MISMATCH&page=0&size=20" \
-H "Authorization: Bearer SESSION_TOKEN"The data payload is a list of ReconciliationResultDto; pagination carries
the page metadata:
{
"data": [
{
"id": 90431,
"mode": "LIVE",
"period": "2026-06-29",
"status": "AMOUNT_MISMATCH",
"transactionRef": "TXN-7Y2K9QF4",
"settlementRecordId": 5521,
"paymentId": 88102,
"paymentReference": "TXN-7Y2K9QF4",
"applicationId": 1042,
"expectedAmountMinor": 150000,
"settledAmountMinor": 149500,
"currency": "BDT",
"detail": "Amount mismatch: internal 150000 vs settled 149500 (minor units)",
"createdAt": "2026-06-30T02:00:01Z"
}
],
"meta": {
"success": true,
"message": null,
"errorCode": null,
"timestamp": "2026-06-30T02:05:00Z"
},
"pagination": {
"page": 0,
"size": 20,
"totalElements": 2,
"totalPages": 1
}
}Result fields
Either side may be absent: settlementRecordId is null when an internal payment
had no settlement row; paymentId/paymentReference are null when a settlement
row had no internal payment.
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | Always | Result row id. |
mode | string | Always | Always LIVE — reconciliation is LIVE-only. |
period | string (date) | Always | The cycle date this result was computed for. |
status | string | Always | One of the five ReconciliationStatus values. |
transactionRef | string | Usually | The reference the two sides are keyed on (the payment tran_id). |
settlementRecordId | integer | Nullable | The matched settlement row; null when an internal payment had no settlement row. |
paymentId | integer | Nullable | The matched internal payment id; null for a settlement-only (MISSING_INTERNAL) row. |
paymentReference | string | Nullable | The matched internal payment's public reference, when found. |
applicationId | integer | Nullable | The owning app, when known from either side. |
expectedAmountMinor | integer (minor units) | Nullable | The internal amount in paisa; null when there is no internal payment. |
settledAmountMinor | integer (minor units) | Nullable | The settled (gross) amount in paisa; null when there is no settlement row. |
currency | string | Usually | ISO currency — BDT in v1. |
detail | string | Usually | A short human-readable explanation for an operator triaging a flagged result. |
createdAt | string (timestamp) | Always | When the result row was written by the run. |
Scheduled runs
A run executes automatically every day at 02:00 UTC, reconciling the
previous day's cycle (by which point that day's settlement report has been
imported — fetching the report from the gateway is an out-of-band ops action). A
failed nightly run is logged and swallowed so one bad night never kills the
scheduler; the next cycle simply tries again. Trigger an on-demand run with
POST /admin/reconciliation/runs (above) when you need to re-reconcile a period
after a late import.
Related
Modes (TEST / LIVE)
Why reconciliation only touches LIVE data.
Money & minor units
Amounts are integer paisa, never floating point.
Transactions
Drill into the payments behind a reconciliation row.
Reports
Settlement and revenue reporting alongside reconciliation.
RBAC
The report:read authority that gates these endpoints.
Dashboard analytics
Operational metrics across apps and modes.
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.
Reports
LIVE totals by application and day with success rate, plus customer analytics (top spenders, new vs returning, repeat-purchase rate).