The Usefulness of a Payment System
Taking money is easy. Taking money reliably, refunding it, reconciling it and staying out of PCI scope is the part that takes a quarter. What a payment system really has to do.
The short version
- Accepting a card is perhaps 10% of the work. Refunds, disputes, retries, reconciliation and reporting are the rest.
- Never let raw card details reach your servers. Hosted fields keep you in the smallest possible PCI scope, and that is worth designing around.
- Payment state lives with the provider and must be mirrored, not owned. Webhooks are the source of truth; the redirect back to your site is not.
- Idempotency is not optional. Networks time out mid-request and the retry must not charge twice.
Integrating payments looks like a two-day job. There is an SDK, there is a form, money appears in an account. Teams schedule it accordingly and then spend the next two months on everything that was not the happy path.
This is an account of what a payment system actually has to do, so you can plan for it before it plans for you.
The happy path is the small part
The full surface of a working payment integration:
- Take a payment, including 3D Secure and other strong customer authentication challenges.
- Handle declines, distinguishing the ones worth retrying from the ones that never will be.
- Refund, in full and in part, and reflect that everywhere the original charge appeared.
- Handle chargebacks and disputes, including gathering evidence within the provider’s deadline.
- Store payment methods for reuse without ever touching the card number.
- Run recurring billing with proration, upgrades, downgrades and cancellations.
- Recover failed recurring payments — dunning — which is directly worth real revenue.
- Reconcile what the provider says happened against what your database says happened.
- Produce invoices and receipts, and apply tax correctly in every jurisdiction you sell into.
- Support multiple currencies and, increasingly, multiple payment methods per market.
Each of these is small. Together they are a quarter of engineering time, and the estimate that omits them is the reason payment projects overrun.
Stay out of PCI scope
The single most consequential architectural decision is ensuring raw card data never touches your infrastructure.
Use the provider’s hosted fields — an iframe rendered by the provider inside your page. The user types into an input your JavaScript cannot read, the details go directly to the provider, and your server receives only an opaque token. Your application handles a reference to a payment method and never the payment method itself.
This keeps you at the lightest PCI DSS compliance tier, which is the difference between a self-assessment questionnaire and a genuine audit programme. The moment card numbers pass through your servers — even transiently, even without storage — your obligations change categorically.
Logging is where this goes wrong. Teams do this correctly in the payment flow and then log a full request body somewhere in middleware, putting card data into a log aggregator with a long retention policy and broad access. Audit what your logging captures, specifically, before you go live.
Payment state is not yours
This is the conceptual shift that causes the most bugs. Your database does not own whether a payment succeeded. The provider does. Your record is a mirror, and mirrors can be stale.
Webhooks are the source of truth
The user completing checkout and being redirected back to your site does not mean the payment succeeded. They may close the tab first. The redirect may fail. The payment may be pending and settle minutes later. Some methods confirm asynchronously by design.
Treat the webhook as authoritative and the redirect as a user-experience convenience. That means:
- Verify the webhook signature on every request, without exception.
- Make handlers idempotent, keyed on the provider’s event ID. Webhooks are delivered at least once and duplicates will arrive.
- Handle out-of-order delivery. A
payment.succeededcan arrive after therefund.createdthat followed it. - Return 200 quickly and process asynchronously. A slow handler causes the provider to retry, which compounds the load.
- Reconcile periodically against the provider’s API anyway, because a webhook you never received is invisible by definition.
Idempotency on the way out
The mirror image of the same problem. You send a charge request, the network times out, and you do not know whether it was processed. Retrying without protection charges the customer twice.
Every serious provider supports an idempotency key: a unique value you generate per logical operation and send with the request. Retries carrying the same key return the original result rather than performing the work again. Generate the key from something stable about the operation — the order ID, not a random value regenerated on retry.
Failed payments are a revenue problem
For subscription businesses, involuntary churn — customers lost to failed payments rather than to any decision — is a large and highly recoverable share of total churn.
The mechanics that recover it:
- Retry on a schedule tuned to decline reasons. Insufficient funds is worth retrying after payday; a stolen card is not worth retrying at all. Providers expose the reason; use it.
- Card account updaters, offered by the networks, automatically refresh details when a card is reissued. This alone eliminates a meaningful slice of failures.
- Pre-expiry notification. Email before the stored card expires rather than after it fails.
- A grace period that keeps access alive while recovery is attempted. Cutting a paying customer off instantly over a bank decline is an expensive way to be right.
Reconciliation
Reconciliation is the process of confirming that what your database believes matches what the provider believes matches what the bank actually settled. It is unglamorous, it is where fraud and bugs surface, and finance will eventually require it whether or not it was planned.
What it needs:
- A stable identifier that links your order to the provider’s charge, stored on both sides.
- A scheduled job that pulls the provider’s settlement report and compares it against your records.
- Explicit handling of fees, which mean the amount settled never equals the amount charged.
- Alerting on discrepancies, with a defined owner. A report nobody reads is not reconciliation.
Build, buy, or buy more
There is a spectrum, and picking the wrong point on it is a common and expensive mistake.
| Approach | You handle | Suits |
|---|---|---|
| Payment gateway | Everything above the charge: subscriptions, invoices, tax, dunning | Teams with specific billing logic and engineering capacity |
| Billing platform | Product catalogue and integration only | Most SaaS businesses, most of the time |
| Merchant of record | Almost nothing; the provider is the legal seller | Selling globally without wanting to own tax registration |
Merchant-of-record services charge visibly more per transaction and absorb global sales tax compliance entirely. For a small team selling into many countries, that is frequently cheaper than the alternative once you price the accountancy.
Boolean Solutions experience with payments
We have integrated payments for e-commerce and subscription products, and been called in afterwards when reconciliation stopped matching or duplicate charges started appearing. Both of those trace back to the same two omissions almost every time: no idempotency keys on outbound requests, and treating the redirect rather than the webhook as confirmation.
Both are straightforward to get right at the start and painful to retrofit once real money has flowed through the wrong path. If you are planning a payment integration, talk to us before you scope it at two days.
Further reading
- Stripe: the Payment Intents API — the clearest explanation of asynchronous payment state anywhere, useful regardless of provider
- Idempotent requests — how and why to use idempotency keys
- PCI Security Standards Council — the source on what compliance actually requires of you
- E-Commerce: The Stages of a Solution — where payments sit in the wider commerce build
