The stakes: the payment layer is the one part of your stack where a bug costs money directly — a shipped order that was never paid, a double-credited wallet refund, a settlement gap nobody notices for a quarter. Vietnamese shops typically run two or three gateways at once (VNPay for cards and bank QR, MoMo and ZaloPay for wallets), and each has its own callback semantics, signature scheme and settlement rhythm. This article covers the patterns that make the whole layer boring — which, in payments, is the goal.
Three integration patterns, one internal model
- Redirect — the customer leaves your checkout for the gateway's hosted page and returns with a result. Simplest to build, and the pattern where the classic security mistake lives (more below).
- QR — you display a payment QR; the customer scans with their banking or wallet app. Great for conversion in Vietnam, but the browser never "returns" — you learn the outcome only from the server-side callback.
- App-to-app / deeplink — mobile checkout hands off to the wallet app and back. Best UX on mobile, most moving parts, and the return leg can be interrupted by anything from a phone call to an OS update.
Whatever mix you run, normalise everything into one internal payment object with one state machine: created → pending → paid / failed / expired → refunded (partial or full). Gateway adapters translate; your order pipeline only ever sees the internal states. This is the same adapter discipline we apply to couriers and platforms across the end-to-end stack.
The IPN is the truth. The redirect is decoration.
Every gateway sends a server-to-server notification (IPN or callback) reporting the payment result, signed with your merchant secret. Three rules are non-negotiable:
- Verify the signature on every IPN — reject anything that fails, and log it, because a failing signature is either a misconfiguration or someone probing your endpoint.
- Verify the amount server-side. Compare the amount in the verified IPN against the amount your order expects. Never mark an order paid based on the customer's redirect back to your site — redirect parameters pass through the customer's browser and can be replayed or tampered with. The redirect page shows a friendly status; the IPN changes the order.
- Make the handler idempotent. Gateways retry IPNs when your endpoint is slow or returns an error — sometimes hours later. Processing the same notification twice must produce exactly the same state, not a second fulfilment or a duplicate ledger entry. Key on the gateway's transaction reference and your order ID together.
Pending is a real state — treat timeouts honestly
A customer scans a QR, their banking app hangs, your checkout times out. A timeout is not a failure. The money may still arrive: the bank transfer completes, the IPN fires two minutes later. If your system marked the order failed and released the stock, you now have a paid order with no goods reserved — and an angry customer. The honest pattern:
Refunds through the API
Manual refunds through gateway dashboards don't scale and leave no trail in your system. Wire refunds through each gateway's refund API, driven from your own order state: a return approved in your admin triggers the refund call, the gateway's response updates the payment object, and the ledger records both legs. Partial refunds (customer kept one of three items) need line-level amounts — design the internal model for that from day one, because retrofitting partial-refund support into a boolean refunded flag is miserable.
Daily settlement reconciliation
Each gateway settles to your bank on its own cycle, minus fees, in batches whose line formats differ. Reconciliation is the job that proves your books match reality:
| Step | What happens | What it catches |
|---|---|---|
| 1. Ingest | Pull each gateway's settlement file or statement for the day | — |
| 2. Match | Match settlement lines to payment objects by transaction reference | Payments the gateway forgot, or you double-counted |
| 3. Fee ledger | Book the fee per line into a per-gateway fees account | Fee changes and per-method cost differences |
| 4. Exceptions | Unmatched lines and amount mismatches go to a review queue | The 0.x% that becomes real money at scale |
Settlement timing differs across gateways — one settles next-day, another on a different cycle — so "yesterday's revenue" never equals "yesterday's bank deposits". Reconcile per gateway on that gateway's cycle, and report cash position separately from revenue. If you also take cash on delivery, this ledger should sit next to your COD remittance matching, covered in the COD reconciliation article — one exceptions queue for all money-in.
Sandbox is not production
Every gateway's sandbox differs from production in ways that matter: instant IPNs in sandbox versus delayed retries in production, relaxed signature validation, test banks that never fail. Passing sandbox tests proves your happy path compiles. Before go-live, run small real transactions through every method — including a deliberate timeout and a refund — and confirm the settlement lines appear and reconcile. The fee lines in particular only become visible with real money.
Where this fits
Once payments emit clean, verified events, everything downstream gets easier: e-invoices are issued against confirmed payments (Shopify → MISA AMIS guide), your daily metrics distinguish authorised from settled revenue (the daily AI briefing pipeline), and the compliance questions around mixed personal/business accounts — a genuine trap — are covered in our compliance round-up.