OI Payments Docs
Admin dashboard

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.)

StatusSides presentMeaning
MATCHEDbothSettlement row and internal payment agree on reference and amount — nothing to do.
AMOUNT_MISMATCHbothMatched by reference but the amounts differ — flagged; figures are never silently adjusted.
MISSING_IN_SETTLEMENTinternal onlyAn internal settled payment has no settlement row and settled too early to be explained by settlement lag — flagged.
MISSING_INTERNALsettlement onlyA settlement row has no matching internal LIVE payment (e.g. present in the report but absent internally) — flagged.
DEFERREDinternal onlySettled 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.

ParamTypeRequiredNotes
perioddate (ISO yyyy-MM-dd)YesThe 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

FieldTypeRequiredNotes
periodstring (date)AlwaysThe cycle date reconciled.
matchedintegerAlwaysItems where settlement and internal records agreed on reference and amount.
missingInSettlementintegerAlwaysInternal LIVE payments absent from settlement beyond the timing tolerance — flagged.
missingInternalintegerAlwaysSettlement rows with no internal payment — flagged.
amountMismatchintegerAlwaysItems matched by reference but disagreeing on amount — flagged.
deferredintegerAlwaysNear-cutoff internal payments expected to reconcile next cycle — not flagged.
ledgerBalancedbooleanAlwaysWhether 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.

ParamTypeRequiredNotes
pageintegerNo0-based page index. Default 0.
sizeintegerNoPage size. Default 20.
sortBystringNoSort field. Default createdAt.
orderASC | DESCNoSort direction. Default DESC.
paginatebooleanNoDefault true; set false to return the full unpaged list.
modestringNoTEST or LIVE. Results are LIVE-only, so LIVE is the only mode with rows; an unrecognized value is rejected with 400.
perioddate (ISO yyyy-MM-dd)NoFilter to a single cycle date, e.g. 2026-06-29.
statusstringNoOne 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.

FieldTypeRequiredNotes
idintegerAlwaysResult row id.
modestringAlwaysAlways LIVE — reconciliation is LIVE-only.
periodstring (date)AlwaysThe cycle date this result was computed for.
statusstringAlwaysOne of the five ReconciliationStatus values.
transactionRefstringUsuallyThe reference the two sides are keyed on (the payment tran_id).
settlementRecordIdintegerNullableThe matched settlement row; null when an internal payment had no settlement row.
paymentIdintegerNullableThe matched internal payment id; null for a settlement-only (MISSING_INTERNAL) row.
paymentReferencestringNullableThe matched internal payment's public reference, when found.
applicationIdintegerNullableThe owning app, when known from either side.
expectedAmountMinorinteger (minor units)NullableThe internal amount in paisa; null when there is no internal payment.
settledAmountMinorinteger (minor units)NullableThe settled (gross) amount in paisa; null when there is no settlement row.
currencystringUsuallyISO currency — BDT in v1.
detailstringUsuallyA short human-readable explanation for an operator triaging a flagged result.
createdAtstring (timestamp)AlwaysWhen 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.

On this page