
Introduction
Reconciliation is the process of matching transactions between internal systems and PSP/bank to ensure the accuracy of financial data. This is a critical function but is often overlooked in payment systems.
1. What is Reconciliation?
1.1 Overview
Reconciliation = So khớp records giữa 2+ hệ thống
Internal Records (Our System) External Records (PSP/Bank)
├── payment_001: 100,000 VND ├── txn_abc: 100,000 VND ✓ Match
├── payment_002: 200,000 VND ├── txn_def: 200,000 VND ✓ Match
├── payment_003: 150,000 VND ├── (missing) ✗ Unmatched
├── (missing) ├── txn_ghi: 50,000 VND ✗ Extra
└── payment_004: 300,000 VND └── txn_jkl: 310,000 VND ✗ Mismatch
1.2 Types of Reconciliation
| Type | Description | Frequency |
|---|---|---|
| Transaction Recon | Match each transaction | Daily |
| Settlement Recon | Match total settlement | Daily/Weekly |
| Balance Recon | Match account balance | Daily |
| Fee Recon | Check the fee according to the contract | Monthly |
| Cross-system Recon | Between internal services | Real-time |
2. Reconciliation Architecture
┌─────────────────────────────────────────────────────────────┐
│ RECONCILIATION ENGINE │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ File │ │ Matching │ │ Exception │ │
│ │ Ingester │────►│ Engine │────►│ Manager │ │
│ └────────────┘ └──────────────┘ └───────────────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌─────▼─────┐ │
│ │ Parser │ │ Rules │ │ Workflow │ │
│ │ (CSV, │ │ Engine │ │ (Manual │ │
│ │ SFTP, │ │ │ │ Review) │ │
│ │ API) │ │ │ │ │ │
│ └─────────┘ └─────────┘ └───────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Reporting & Dashboard │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.1 File Ingestion
@Service
public class ReconciliationFileIngester {
// PSPs provide settlement files in various formats
public List<ExternalTransaction> ingest(PSPFileConfig config) {
return switch (config.getFormat()) {
case CSV -> parseCsv(config);
case SFTP_CSV -> downloadAndParseCsv(config);
case API -> fetchViaApi(config);
case EXCEL -> parseExcel(config);
};
}
// VNPay sends CSV via SFTP
private List<ExternalTransaction> downloadAndParseCsv(PSPFileConfig config) {
var file = sftpClient.download(
config.getHost(), config.getPath(),
config.getCredentials());
return csvParser.parse(file, VNPayTransactionMapper.class);
}
}
2.2 Matching Engine
@Service
public class TransactionMatcher {
public ReconciliationResult match(
List<InternalTransaction> internal,
List<ExternalTransaction> external) {
var result = new ReconciliationResult();
var externalMap = external.stream()
.collect(Collectors.toMap(
ExternalTransaction::getPspReference, Function.identity()));
for (var txn : internal) {
var ext = externalMap.remove(txn.getPspReference());
if (ext == null) {
result.addUnmatched(txn); // Missing on PSP side
} else if (!txn.getAmount().equals(ext.getAmount())) {
result.addMismatch(txn, ext); // Amount mismatch
} else if (!txn.getCurrency().equals(ext.getCurrency())) {
result.addMismatch(txn, ext); // Currency mismatch
} else {
result.addMatched(txn, ext); // Perfect match
}
}
// Remaining external transactions = not in our system
for (var ext : externalMap.values()) {
result.addExtra(ext);
}
return result;
}
}
3. Exception Handling
3.1 Exception Types
Reconciliation Exceptions:
├── MISSING_EXTERNAL: In our system, not in PSP
│ → PSP may not have processed yet (timing)
│ → PSP failed silently
│
├── MISSING_INTERNAL: In PSP, not in our system
│ → Our system crashed before recording
│ → Duplicate on PSP side
│
├── AMOUNT_MISMATCH: Different amounts
│ → Partial capture not reflected
│ → FX rate difference
│ → Fee deducted at PSP
│
└── STATUS_MISMATCH: Different statuses
→ Async update not received
→ Webhook missed
3.2 Resolution Workflow
Exception Detected
│
▼
Auto-resolution possible?
│
├── YES: Apply auto-fix
│ ├── Timing issue → Wait and re-check
│ ├── Missed webhook → Query PSP for status
│ └── Known pattern → Apply standard fix
│
└── NO: Create manual review task
├── Assign to operations team
├── Set SLA (24h for critical, 72h for normal)
└── Escalate if unresolved
4. Settlement & Payout
4.1 Merchant Payout Flow
Daily Settlement:
1. Aggregate captured transactions per merchant
2. Calculate fees (platform fee, PSP fee)
3. Calculate net payout amount
4. Create payout record
5. Submit bank transfer
6. Update settlement status
Payout Schedule:
├── T+1: Next business day (standard)
├── T+0: Same day (premium merchants)
├── Weekly: Every Monday (small merchants)
└── On-demand: Instant payout (with fee)
4.2 Payout Implementation
@Service
public class PayoutService {
@Scheduled(cron = "0 0 8 * * MON-FRI") // 8:00 AM weekdays
public void processPayouts() {
var pendingPayouts = payoutRepository
.findByStatus(PayoutStatus.PENDING);
for (var payout : pendingPayouts) {
try {
// Transfer via bank API
var transferResult = bankApi.transfer(
payout.getMerchantBankAccount(),
payout.getNetAmount(),
payout.getReference());
payout.markProcessed(transferResult.getReference());
payoutRepository.save(payout);
notificationService.notifyMerchant(
payout.getMerchantId(),
"Payout processed: " + payout.getNetAmount());
} catch (BankApiException e) {
payout.markFailed(e.getMessage());
payoutRepository.save(payout);
alertService.alert("Payout failed: " + payout.getId());
}
}
}
}
Summary
Reconciliation & Settlement is the backbone of FinTech operations:
- Automated matching between internal records and PSP data
- Exception management with auto-resolution and manual review
- Settlement batch daily with fee calculation
- Payout processing with bank integration
Next article: Digital Wallet Architecture — E-Wallet & Balance Management.