Underpayment and Overpayment Handling in the Philippines
I run the payments side of a China-based merchant operation that sells into the Philippines, and most of our collection volume does not come through a card form or an e-wallet checkout button. It comes through bank transfer and over-the-counter (OTC) channels — a customer walks into a partner outlet, hands cash to a teller, or opens their banking app and keys in a transfer manually. That one difference, a human typing an amount instead of a checkout page enforcing it, is the root of almost every underpayment and overpayment ticket my team deals with. This article is the detection and handling framework we eventually settled on, written for anyone building or operating a merchant backend on top of OTC or bank-transfer collection in the Philippines.
Why OTC and Bank Transfer Break the "Exact Amount" Assumption
With a card or an e-wallet checkout flow, the payment page itself enforces the amount. The customer cannot submit a different number than what the order requires; the authorization either matches the charge or it fails. That constraint simply does not exist once you move to OTC payment channels like 7-Eleven, Cebuana, and bank deposit counters or a direct bank transfer. In both cases, the customer (or a teller acting on their behalf) is the one entering the figure, and your system only finds out what actually landed after the fact, through a settlement file or a notification feed.
That gap between "what we invoiced" and "what arrived" is where a huge share of OTC and bank-transfer support tickets originate. It is not a minor edge case for merchants who lean on these rails — for a meaningful part of the Philippine market, OTC and bank transfer are the primary or only way a customer can pay, especially outside the major wallets. If your business depends on that segment, you will see amount mismatches often enough that handling them needs to be a designed system, not an ad hoc response from whoever is on shift when a ticket comes in.
Why the Amount Doesn't Match: The Common Failure Patterns
After enough tickets, the mismatches stop looking random. A handful of patterns account for almost everything we see:
- Rounding to a convenient figure. A customer transferring cash at a counter will round to the nearest bill denomination they are carrying rather than hunt for exact change, so the order total and the transferred total differ by a small margin.
- A fee deducted at the origin. Some OTC agents or remittance counters subtract their own service charge from the amount the customer hands over, so what you receive on the back end is already net of a fee the customer assumed you would absorb or were never told about.
- Split or installment intent. A customer planning to pay in two transfers sends the first portion, which your system sees as a plain underpayment rather than "part one of two."
- Manual transcription error. A teller mistypes a digit, or the customer misreads the invoice and transposes two numbers.
- Decimal or unit confusion. Occasionally a customer enters the amount with the decimal point shifted, producing a payment that is a clean multiple too large or too small.
- Duplicate submission. A customer unsure whether their first transfer went through sends a second one, which shows up as a large overpayment rather than two separate attempts.
None of these require fraud intent. They are what you should expect, structurally, any time a human is the one keying in the number — which is exactly why the system needs to be designed around that reality instead of treating every mismatch as an exception.
Designing the Detection Layer: Exact Match, Tolerance Band, Manual Review
The detection logic we run sits between the settlement feed (the list of actual incoming transfers or OTC remittances) and the order record. It runs in three tiers:
- Exact match. The received amount equals the expected amount. This is the fast path — auto-confirm, mark the order paid, move on. Most card and wallet transactions never leave this tier; a healthy share of bank-transfer and OTC transactions do too.
- Tolerance-band auto-match. The received amount differs from the expected amount, but the gap sits inside a pre-defined tolerance band — sized to absorb routine rounding or a known OTC fee deduction, but tight enough that it would not plausibly hide a real error. Transactions in this band are auto-matched and closed without a human touching them, with the small delta logged against a write-off or fee-absorption ledger entry rather than silently disappearing.
- Manual review queue. Anything outside the tolerance band — in either direction — gets pulled out of the automated path entirely. The order is held, not auto-confirmed and not auto-cancelled, and a reviewer sees the expected amount, the received amount, the delta, and the payment method, then decides the next step using the decision trees below.
A few design details matter more than they look like they should. First, tolerance should be configurable per payment method rather than global — an OTC channel with a known fee-deduction pattern needs a different band than a direct bank transfer, where a mismatch is more often a genuine error than a fee artifact. Second, every auto-match and every manual decision needs a reason code written to the ledger, because without one your reconciliation workflow degrades into someone re-deriving the story from memory weeks later when finance asks why the books don't balance. Third, this detection layer should run against whatever settlement timing your underlying rail actually has — for same-day rails the mismatch surfaces quickly, but for others there can be a lag, and your hold logic has to account for that lag rather than assuming the mismatch is visible the moment the customer clicks "I've paid."
The Underpayment Decision Tree
Once a payment is flagged as underpaid, we work through it in a fixed order rather than improvising per ticket:
- Inside tolerance: auto-match, no customer contact needed, delta logged.
- Outside tolerance, plausible explanation (small gap, known OTC channel): hold the order in an unfulfilled state, notify the customer of the shortfall, and offer a simple way to send the difference — a top-up link or a reference number tied to the same order. Fulfillment waits until the top-up clears or the window expires.
- Outside tolerance, no plausible fee explanation (meaningful gap on a direct bank transfer): treat it as a genuine error. Hold the order, contact the customer, and give them the choice of topping up or receiving a refund of the partial amount if they no longer want the order.
- No response within a reasonable window: refund the partial amount back to the originating account where traceable, and close the order. Never let a partial payment sit indefinitely as neither fulfilled nor resolved — that state is exactly what support escalations and later disputes come from.
Partial fulfillment is tempting to reach for because it feels fairer than making the customer wait, but it only makes sense for genuinely divisible goods or services — a prepaid balance you can credit proportionally, for instance. For a fixed-quantity order, partial fulfillment usually creates more reconciliation and inventory problems than it solves, and a hold-until-corrected approach with a clear deadline tends to be the simpler, more defensible default.
The Overpayment Decision Tree
Overpayment handling needs the same discipline, with one principle sitting above everything else: the business does not get to unilaterally decide what happens to a customer's excess money.
- Inside tolerance: treat as a match, no action, small delta logged as a rounding gain.
- Modest overpayment outside tolerance: the default action is to refund the difference back to the original payment source where that source is identifiable. Crediting the excess toward a future order is a legitimate alternative, but only with the customer's explicit, logged consent — never as a silent default because it's operationally easier for you.
- Large or pattern-matching overpayment (a round-number duplicate, an amount that looks like two orders paid as one, or a sequence of overpayments from related accounts): route to a compliance or risk review before any refund goes out, rather than auto-processing it. This is also where a practical fraud-detection rule framework earns its keep, since unexplained overpayment clusters are a pattern worth watching rather than dismissing as customer error.
Whichever default you pick, write it into your customer-facing terms so it isn't a surprise the first time someone overpays. And when the resolution is a refund, make sure your team understands the technical side of issuing a refund on the rail in question — OTC and bank-transfer refunds often cannot just reverse the same transaction the way a card refund can; they frequently need a separate payout leg back to a verified account.
We operate this reconciliation layer on top of infrastructure licensed by the BSP as an Operator of Payment Systems (OPS) and Electronic Money Issuer (EMI), which is what gives us a defined settlement and refund rail to hang this logic on in the first place — without that, "refund the difference" is just a promise, not a process.
Where a Licensed Payment Infrastructure Changes the Calculus
A lot of the underpayment and overpayment advice I've seen online assumes you're wiring logic around an informal bank account arrangement, where every exception gets resolved by someone manually calling a bank branch. That approach does not scale, and it leaves you with no real audit trail when a customer disputes what happened to their money. Running collection through a properly licensed gateway means the settlement feed your detection layer reads from is structured and timestamped, the refund path is a defined API call rather than a manual bank request, and the whole chain — from received amount to resolution to ledger entry — is something you can actually produce if a regulator, an auditor, or an unhappy customer asks for it. If you're building this on top of same-day settlement mechanics, the detection and hold logic also needs to run on that same cadence, so a mismatch doesn't sit unresolved for longer than the settlement cycle itself would justify.
Whatever gateway you build this on, confirm it actually holds BSP OPS or EMI authorization — the refund and payout rails that underpayment and overpayment handling depend on are not something you can assume exists behind an unlicensed aggregator.
Mismatch Types at a Glance, and a Reconciliation Pattern to Adapt
Before the code, it helps to see the common cases side by side:
| Mismatch Type | Typical Cause | Typical Handling | Risk if Ignored |
|---|---|---|---|
| Small underpayment (rounding) | Customer rounds to a convenient note or figure | Tolerance-band auto-match, delta logged as write-off | Manual review queue clogs with trivial cases, slowing real exceptions |
| Fee-deducted underpayment | OTC agent or remittance counter subtracts its own charge | Tolerance band sized for that channel, or auto top-up request | Customer disputes an order marked unpaid when they believe they paid in full |
| Large underpayment | Transcription error, wrong invoice read, partial/installment intent | Hold order, request difference, refund partial amount if no response | Order fulfilled against a shortfall, or indefinitely stuck in limbo |
| Small overpayment (rounding up) | Customer sends a round figure above the total | Tolerance-band match, delta logged as gain | Minor accounting drift if not logged consistently |
| Large overpayment / duplicate | Duplicate transfer, wrong amount entered, two orders paid as one | Hold for review, refund to source or credit with explicit consent | Customer trust damage, potential compliance exposure if pattern repeats |
| Split/installment payment | Customer intends to pay in stages, system expects one lump sum | Hold with a top-up window instead of flat rejection | Legitimate customers treated as underpaying and churned unnecessarily |
The core of the detection layer behind that table is a small comparison function that sits in your reconciliation job, run against every incoming settlement record. The values below are placeholders to show the shape of the logic, not real transaction figures:
def classify_payment(expected_amount, received_amount, tolerance_amount, method):
delta = received_amount - expected_amount
if delta == 0:
return {"status": "matched", "action": "auto_confirm", "delta": delta}
if abs(delta) <= tolerance_amount:
action = "auto_confirm_overpaid" if delta > 0 else "auto_confirm_underpaid"
return {"status": "tolerance_match", "action": action, "delta": delta}
if delta < 0:
return {
"status": "underpaid",
"action": "hold_request_topup",
"delta": delta,
"method": method,
}
return {
"status": "overpaid",
"action": "hold_for_refund_or_credit_consent",
"delta": delta,
"method": method,
}
# Example call — placeholder values only
result = classify_payment(
expected_amount=15000.00,
received_amount=14820.00,
tolerance_amount=250.00,
method="otc_bank_deposit",
)
# result => underpaid, hold_request_topup, delta=-180.00
In production this function feeds a ledger write, not just a return value: every call writes the expected amount, received amount, delta, method, and resulting action to an append-only reconciliation log, so a finance reviewer can reconstruct any decision later without reverse-engineering it from support chat logs.
Frequently Asked Questions
Should the tolerance band be the same for every payment method?
No. A channel with a known, consistent fee-deduction pattern needs a tolerance sized around that pattern; a direct bank transfer with no intermediary fee should run a tighter band, because a mismatch there is more likely to be a genuine error worth catching.
What happens if a customer never tops up an underpayment?
Set a clear window — long enough to be fair, short enough that orders don't sit open indefinitely — after which the partial amount is refunded to the originating account where traceable and the order is closed. An order should never remain in an unresolved held state with no expiry.
Can I just keep a small overpayment instead of refunding it?
Not as a default. Treat it as the customer's money until they've agreed otherwise. For amounts inside your tolerance band it's reasonable to log it as a rounding gain without contacting the customer, but anything beyond that needs either a refund or explicit, logged consent to apply it as credit.
How do I avoid two legitimate orders fighting over one ambiguous transfer?
Require a reference number or order ID in the transfer description wherever the rail supports it, and when it's missing or ambiguous, route the transaction to manual review rather than guessing which order it belongs to based on amount alone.
Should refunds go back to the original sender, or can I pay out to a different account?
Default to the original source whenever it's identifiable — that's the safest path from both a customer-trust and an anti-fraud standpoint. Paying out to a different account the customer specifies should require additional verification before you treat it as routine.
Does tolerance-band auto-matching increase fraud risk?
Only if the band is set too wide or applied blindly to every method. Kept narrow and method-specific, it mainly filters out routine rounding noise so your manual review queue is left with the cases that actually warrant a human look.
How does this interact with disputes raised later by the customer's bank?
Keep your reconciliation log detailed enough — expected amount, received amount, delta, timestamp, and the action taken — that you can answer a dispute with a clear record rather than a reconstructed guess. That log is also what you'll want on hand if a dispute escalates to a formal chargeback-style process.
If you're building or auditing this layer for an OTC- or bank-transfer-heavy business in the Philippines, I'm happy to compare notes on tolerance design and the decision-tree logic above. Reach me on Telegram at @LouisSMARTPAY or by email at [email protected].