Refunds in the Philippines: Process and Technical Implementation
When I first built the refund flow for our Philippines storefront, I assumed it would be a simple reversal of the original charge. It isn't. Once customers are paying through GCash, Maya, QRPH, cards, and over-the-counter channels like 7-Eleven or a bank counter, "refund" actually describes several different operations that happen to share one button in an admin panel. Each rail has its own rule for where the money goes back to, how fast it moves, and what happens when the original payment method doesn't actually have an "account" to receive money into. This is the write-up I wish our team had before we shipped our first refund button.
The Refund Problem in a Multi-Rail Payment Market
In a single-rail market, a refund is almost mechanical: call one API, reference one transaction ID, and the processor pushes money back to the same card or account. The Philippines is not a single-rail market. A customer checking out on the same page might pay through an e-wallet, a debit card, a credit card, or cash handed to a cashier at a convenience store. Each sits on a different settlement rail with a different operator, a different ledger structure, and a different answer to "can this specific payment be reversed, and to where?"
A refund is not a new payment in the opposite direction — it's a correction tied to a specific original transaction, and the rail that carried the money in usually has opinions about how it comes back out. Treat all refunds as interchangeable, and you'll eventually hit a channel where that assumption doesn't hold, most commonly with OTC-originated payments, which I'll get to below.
Full Refunds vs. Partial Refunds: What Changes Under the Hood
A full refund is the easier case. The gateway takes the original transaction, marks it reversed, and moves the full captured amount back through the same rail. Most APIs let you call this with nothing more than the original transaction reference — no new amount field required, because the system already knows what was captured.
A partial refund is where the implementation gets interesting, because the transaction record now has to support a state that isn't binary. You're no longer asking "was this paid or reversed" but "how much of this transaction has been refunded so far, and is there anything left." That means your data model needs the original transaction amount, a running total refunded against it, and a check that rejects any refund request pushing the cumulative total past the original capture.
The failure mode I've seen most often is a refund button that lets a support agent issue a second partial refund without checking what was already refunded against that order, because the UI queried the order total instead of querying refund history against that specific transaction ID. If your refund ledger isn't keyed to the original transaction, over-refunds slip through, and they're painful to claw back once the money has left on an e-wallet rail.
Partial refunds also interact with however you're doing payment API error handling on the rest of your stack. A refund call that times out on your side but actually succeeded upstream is a different failure than one that genuinely failed, and your retry logic needs to tell them apart, usually by re-querying refund status with an idempotency key rather than blindly resubmitting.
Why Refunds Route Back to the Original Payment Method (and When They Can't)
Almost every rail in the Philippines payment ecosystem enforces the same rule: a refund goes back to the account, card, or wallet that funded the original transaction, not to an account of the customer's choosing. This isn't a UX decision gateways made to be inflexible — it's a compliance control. Letting a merchant redirect outgoing funds to an arbitrary destination account is exactly the pattern anti-money-laundering monitoring is built to catch, because it's indistinguishable from layering funds through a merchant account into an unrelated third party.
Both BSP OPS and EMI licensees (see what a BSP EMI license actually covers) operate under reporting obligations that make "refund to original source" the default behavior, not an optional setting — part of why licensed gateways like SMARTPAY don't expose a free-text destination field for refunds the way a generic transfer feature might.
For e-wallets and cards this is straightforward: the wallet ID or card token from the original transaction is still on file, and the refund instruction references it directly. The harder case is OTC payment channels like 7-Eleven, Cebuana, and bank counters, because the customer paid with cash at a physical counter — there is no wallet, no card, and no account number tied to that transaction. The payment reference only ever pointed to a biller code and a cash transaction at the counter, not to any identifiable financial account of the payer.
When that happens, there is no automated "push money back the way it came" option, because there is no "where it came from" in the electronic sense. In practice this forces one of three fallback paths: a manual disbursement to a bank or e-wallet account the customer provides after verification, a store credit issued on the merchant's own platform, or holding the refund as a ledger adjustment against a future transaction. None of these are technically a "refund" in the rail sense — they're compensating transactions your system has to model separately, with their own approval and audit trail, precisely because they bypass the usual original-method control.
Refund Timing Across GCash, Maya, QRPH, Cards, and OTC-Originated Payments
Timing is the other place where treating refunds as one undifferentiated process causes support tickets. E-wallet refunds tend to move on the wallet operator's own settlement batches — usually the fastest path available, but still a batch process, not a synchronous transfer. Your system showing "refund issued" and the customer's wallet balance updating are two separate events. Card refunds are structurally slower, traveling back through the acquirer to the card network and then the issuing bank, with each hop running its own batch cycle; this is the same network path involved in 3D Secure authentication for card payments, just running in reverse. QRPH refunds depend on which participant bank or wallet is on the receiving end, since QRPH is a standard multiple institutions implement, not one operator with a uniform internal clock.
OTC-originated refunds are the outlier: there usually isn't a direct reversal path at all, so "timing" really means how long your manual or alternate-disbursement process takes — closer to a customer service SLA than a payment rail SLA.
| Channel | Refund Route | Typical Processing Pattern | Partial Refund Support | Key Technical Note |
|---|---|---|---|---|
| GCash / e-wallets | Back to the originating wallet ID | Follows the wallet operator's settlement batch; generally the fastest rail available | Supported via amount field against original transaction ID | Refund status should be confirmed via webhook/callback, not assumed from the API response alone |
| Maya | Back to the originating wallet account | Similar batch-based pattern to other e-wallets; depends on Maya's internal processing window | Supported, referenced to original transaction | Treat refund confirmation and payment confirmation as separate webhook events |
| QRPH | Back to the originating participant account (bank or wallet) | Varies by which bank or wallet issued the original QR payment; no single uniform clock across participants | Supported where the participant institution's refund API allows it | Refund capability can differ by participant bank — verify support before promising it at checkout |
| Cards (debit/credit) | Back to the original card via acquirer and issuing bank | Generally the slowest rail; involves acquirer processing plus the issuing bank's own posting cycle | Supported; most processors allow multiple partial refunds up to the captured amount | A refund can show "processed" on the merchant side well before it posts on the cardholder's statement |
| OTC-originated (7-Eleven, Cebuana, bank counter) | No direct reversal path to the original cash payment | Not rail-driven; depends on manual disbursement or alternate compensation method | Requires manual tracking — not a standard API refund call | Needs identity verification before releasing funds to any destination account |
Issuing a Partial Refund: A Practical API Example
Most gateway refund endpoints share the same shape: reference the original transaction, specify the refund amount, and attach a merchant-side reference to match the response to your own order record. Here's a representative partial refund call:
curl -X POST https://api.gateway-example.com/v1/refunds \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
-H "Idempotency-Key: refund-ORDER20261004-01" \
-d '{
"original_transaction_id": "TXN-88213749",
"merchant_order_id": "ORDER20261004",
"refund_amount": "350.00",
"reason": "customer_requested_partial_return",
"notify_url": "https://merchant.example.com/webhooks/refund-status"
}'
A few details matter more than they look. The Idempotency-Key exists so a network timeout on your side doesn't result in the same refund being submitted twice — resubmitting with the same key should return the existing result rather than creating a duplicate reversal. The original_transaction_id ties this refund to a specific captured payment rather than to the order as a whole, which is what makes multiple partial refunds against one order possible without double-counting. And the response should be treated as provisional, not final — the notify_url webhook is where you find out whether the refund actually cleared, especially for cards and QRPH, where the first response often means "accepted for processing" rather than "completed."
A typical response body includes a refund ID, a status field (pending, processing, or completed), and the running total refunded against the original transaction. That running total is what your order management system should store and check on every subsequent refund attempt, not a value recalculated from the order's current displayed balance.
Designing a Refund Policy Your Payment Stack Can Actually Support
A lot of refund policy gets written by customer service or legal without checking what the underlying channels can technically do, leaving engineering to quietly work around the gap. A few things worth deciding before writing the policy, not after:
- Will you promise refunds for OTC-originated orders at all, and what's the actual fallback process when there's no electronic account to reverse the payment to?
- Do you allow multiple partial refunds against one order, or only one partial followed by a full close-out? This changes what your refund ledger needs to track.
- What's your threshold for requiring a second approver on a refund — enforced in the API layer, or only in a manual step someone can skip under pressure?
- How do you handle a refund request arriving after the settlement window has closed for that transaction: a new-period adjustment, or rejected and redirected to manual compensation?
None of these are purely technical questions, but each produces a technical requirement. A policy that says "any amount, any time, any channel" without checking what GCash, Maya, QRPH, card acquiring, and OTC channels each actually permit is one your engineering team will end up quietly rewriting through workaround after workaround.
Reconciliation: Tying Every Refund Back to Its Original Transaction
Once refunds are flowing, the question shifts from "can we issue this refund" to "can we prove, in our books, exactly which original transaction this refund corresponds to, for how much, and through which rail." This is where a lot of merchants get burned months later during an audit, not at the point of issuing the refund itself.
The minimum reconciliation record for a refund should carry: the original transaction ID, the refund ID, the channel, the amount, the timestamp requested versus the timestamp confirmed by the rail, and the running cumulative refunded amount against that transaction. Without that last field, two partial refunds can look identical and get accidentally merged or double-counted at month-end close.
This connects directly into how payment records and tax reporting work for Philippines e-commerce merchants — a refund isn't just a customer service event, it's a reversal of recognized revenue that has to show up correctly in your books under the same reporting period logic that governs the original sale. Without a clean link back to the original transaction and its settlement date, your accounting team ends up reconstructing that link manually from gateway statements, which is slow and error-prone at any real transaction volume.
The practical fix is almost always the same: never let a refund exist in your system as a standalone record. Every refund row should carry a foreign key to the original transaction, not just a human-readable order number, and your daily reconciliation job should flag any refund that can't be matched automatically, rather than letting it sit as an exception someone has to notice by hand.
Frequently Asked Questions
Can we refund a customer to a different account than the one they paid from?
Generally no, not through the standard refund API. Licensed gateways route refunds back to the original payment source by default, as part of the compliance controls tied to their BSP OPS/EMI license. Redirecting to a different destination usually requires a separate manual disbursement with its own identity verification.
How long does a refund actually take once we call the API?
It depends heavily on the rail. E-wallet refunds generally move on the wallet operator's own settlement batches and tend to be the fastest option available. Card refunds are usually slower, passing through the acquirer and the issuing bank's own posting cycle before showing up on a statement. There's no single fixed timing across all channels, which is why it's worth setting expectations per channel rather than with one blanket promise.
What happens when a customer paid through an OTC channel like a 7-Eleven counter?
There's no electronic account tied to that original payment, so there's no automated reversal path. The common fallback is a manual disbursement to a bank or e-wallet account the customer provides after verification, or a store credit on the merchant's own platform.
Does a partial refund need its own reconciliation record?
Yes. Each partial refund should be its own row tied to the original transaction ID, with the running cumulative refunded amount tracked separately from the order's displayed balance. Treating partial refunds as informal adjustments is the most common cause of reconciliation mismatches at month-end.
Why would a refund show as "success" on our side but the customer says they haven't received it?
Because the initial API response often confirms the refund was accepted for processing, not that it has cleared on the underlying rail. The webhook callback to your notify_url is usually where the actual completion status arrives, and it's worth building order status logic around that callback rather than the initial response alone.
Can a refund be reversed once it's processed?
In most cases, no — once funds have moved back through the rail to the customer's wallet, card, or account, there's no standard API call to pull them back. This is part of why approval thresholds and reconciliation checks matter before a refund is submitted, not after.
Refunds look like a formality until the first real dispute or audit, at which point they're the part everyone is suddenly staring at. If your team is working through this on a live GCash, Maya, QRPH, or card integration and wants to compare notes on the OTC edge case or partial-refund reconciliation, reach out to @LouisSMARTPAY or [email protected].