Payment software development is mostly not about moving money. The provider moves the money. What you build is the record of who is owed what, the logic that decides when money should move, and the checks that prove it moved the way your records say it did. Get those three right and the provider can be swapped. Get them wrong and no provider will save you.
Founders usually arrive with a feature list: take card payments, add a wallet, pay out to sellers, issue refunds. Each item sounds like an integration. In practice each one is a small accounting system wearing an API, and the bugs that hurt are accounting bugs: a refund issued twice, a payout sent before the funds settled, a wallet balance that drifts a few rupees from reality and nobody can explain why.
This guide walks through what payment software is made of, which parts you rent and which you must build, and the engineering decisions that decide whether your numbers still agree in a year. It is written for teams commissioning payment features inside a product, a marketplace or a fintech app, not for anyone building a card network.
Payment software is four layers, and you build at most two
Every payment product, from a checkout page to a wallet app, sits on the same stack. The first decision is where your build starts:
| Layer | What it does | Buy or build |
|---|---|---|
| Networks and banks | Card schemes, bank rails, UPI, settlement between banks | Never build. You reach them through a provider. |
| Payment provider | Collects card and bank details, talks to the networks, settles funds to you, handles chargebacks | Buy. Building a gateway means becoming a regulated payments company. |
| Ledger and money logic | Records every movement, holds balances, decides when to capture, refund, hold or pay out | Build. This is your business rules, and no provider knows them. |
| Product and operations tools | Checkout, wallet screens, payout schedules, finance dashboards, dispute handling | Build, using the provider's hosted pieces where they fit. |
Most failed payment projects either try to build the provider layer, which turns a product into a compliance programme, or skip the ledger layer and treat the provider's dashboard as the source of truth. The second mistake is far more common and far more expensive, because it only shows up months later when finance asks a question the dashboard cannot answer.
Choose the provider by what it must do, not by its fee
The headline transaction fee is the easiest number to compare and the least important. A provider that is a few basis points cheaper but cannot do one thing your product needs will cost you a rebuild. Before comparing prices, write down answers to these:
- Where are your customers and where are you? Providers differ by country on which payment methods they support, which currencies they settle in, and whether they can onboard your entity at all.
- Which payment methods matter? Cards, UPI, bank transfers, local wallets, buy now pay later. A provider that is strong on cards may be weak on the method your customers actually use.
- Do you pay anyone out? Paying sellers, drivers or service providers is a separate product at most providers, with its own onboarding, identity checks and fees. If you need it, check it first.
- Do you need to hold funds? Some products collect now and release later, after a delivery, a service or a signed contract. Whether the provider supports holding funds, for how long, and in whose name decides your architecture.
- How fast do funds settle, and what does the settlement report look like? You will reconcile against that report every day. A provider with a clear, downloadable settlement file is worth a lot.
Pick one provider for launch, but design so a second one could be added. In practice that means your own code talks to an internal payments module, and only that module knows which provider exists. Provider IDs are stored as references on your own records, never used as your primary keys.
The ledger is the part you cannot rent
A ledger is the record of every movement of money in your system. It is the one piece every serious payment product builds for itself, and the one most often built badly as a single balance column that code adds to and subtracts from. That column works until two requests update it at the same moment, or a retry runs twice, or someone asks why a balance is what it is. Then it has no answer.
The pattern that holds up is double-entry: every movement is written as entries that move an amount out of one account and into another, and the entries for a movement always sum to zero. Balances are never stored as the truth. They are calculated from the entries, and cached only for speed.
Here is a customer topping up a wallet and then spending part of it with a seller, as ledger entries:
| Event | Account | Debit | Credit |
|---|---|---|---|
| Top-up received | Cash held at provider | 500.00 | |
| Top-up received | Customer wallet | 500.00 | |
| Purchase | Customer wallet | 300.00 | |
| Purchase | Seller payable | 270.00 | |
| Purchase | Platform fee revenue | 30.00 |
The customer's wallet now shows 200.00, the seller is owed 270.00, and the platform has earned 30.00, and every one of those numbers can be traced to the entries that produced it. When the seller is paid out, two more entries move 270.00 out of seller payable and out of cash. Nothing is ever edited or deleted. A mistake is corrected with new entries that reverse it, so the history of the mistake survives.
Three rules keep a ledger honest:
- Entries are immutable. No update, no delete. Corrections are new entries.
- Amounts are integers in the smallest unit. Paise, cents, never floating point. Floating point rounding is how balances drift by fractions nobody can find.
- Every entry points to its cause. The order, the refund, the payout, the provider's transaction reference. An entry with no cause is a question you will be asked in an audit.
Every money call must be safe to retry
Networks fail in the worst possible way for payments: the request reaches the provider, the money moves, and the response never comes back. Your code sees a timeout. If it retries blindly, the customer is charged twice. If it gives up, your records say the payment failed when it did not.
The fix is idempotency. Every request that moves money carries a unique key that your system generates and stores before the call, such as the order ID plus the attempt type. Providers that support idempotency keys return the original result when they see the same key again, rather than charging a second time. Your own endpoints should do the same for the same reason: a mobile app on a poor connection will resend a button press.
The second half of the fix is admitting that a payment can be in an unknown state. Model each payment as a set of explicit states, for example created, pending, authorised, captured, failed, refunded, and add one that most systems forget: unknown. A payment that timed out is unknown, not failed. A background job asks the provider what really happened and moves it on. Until then, nothing downstream, no shipping, no wallet credit, no payout, acts on it.
Concurrency needs the same care. Two requests that spend from the same wallet at the same moment must not both see the old balance and both succeed. Write the spend inside a database transaction that locks the account, or use a check in the insert itself that refuses an entry which would take the balance below zero. Whichever you choose, test it by firing the same spend many times in parallel and confirming only the right number succeed.
Provider notifications, usually webhooks, are how most of those state changes arrive. They can come late, out of order or more than once, so the handler must check the current state before acting and ignore a notification it has already processed. Our post on Stripe Connect split payments goes into webhook handling, refunds and disputes for marketplaces in detail, including how Nestlet held funds until a contract was signed.
Reconciliation is how you find out you were wrong
Reconciliation means comparing your ledger to what actually happened outside it, every day, automatically. It is the feature nobody asks for and every finance team ends up needing, usually after the first month-end that does not balance.
The comparison runs on three records:
- Your ledger: what you believe happened.
- The provider's settlement report: what the provider says it collected, refunded, charged in fees and paid to you.
- Your bank statement: what actually arrived.
Match each line to its counterpart by reference, and report anything unmatched. The causes of mismatches are predictable, which means each one can be handled by a rule rather than a person:
| Mismatch | Usual cause | How to handle it |
|---|---|---|
| Bank received less than you charged | Provider fees deducted before settlement | Record fees as their own ledger entries from the settlement report |
| Amounts differ slightly in another currency | Conversion at a rate you did not store | Store the rate and both amounts on every cross-currency entry |
| Refund amount does not match | A partial refund logged as a full one | Refunds always carry their own amount, never copy the original |
| Payment in the ledger, missing from the report | Captured late on one day, settled the next | Match on a rolling window, flag only after it closes |
| Payment in the report, missing from the ledger | A webhook that never arrived | Create the entry from the report and log the gap |
Build reconciliation in the first release, even if the first version only produces a daily list of mismatches for a person to read. A mismatch found the next morning has one day of activity around it and a fresh trail. The same mismatch found at the end of a quarter is buried under months of other movements, and the people who remember the cause may have moved on.
Wallets: decide what the balance legally is before you build it
"Virtual wallet" covers two very different products, and the difference is legal before it is technical.
Closed-loop credit
Store credit, loyalty points, prepaid credit that can only be spent with you. The customer cannot withdraw it or send it to someone else. This is the simpler case: it is a ledger account per customer, with rules for how credit is earned, spent and expires. Most products that say they want a wallet actually want this.
Stored money that can move
A balance the customer can top up, send to other users, spend at other merchants or withdraw to a bank. Holding customer money like this is regulated in most markets, India included, and usually requires a licence or a licensed partner who holds the funds. The rules cover how funds are safeguarded, identity checks on users, limits, and reporting. This decision belongs with a lawyer before it reaches an engineer, because it decides whether the money sits in your account at all.
In both cases the engineering underneath is the same ledger: one account per wallet, every top-up, spend, transfer and withdrawal written as balanced entries, and the balance calculated from them. Wallet-specific work sits on top: transfer limits, holds while a top-up settles, reversing a transfer sent to the wrong user, and statements a customer can read. If a wallet is on your roadmap, our e-wallet app development page is a good place to start, and the first question to settle is which of the two wallets you actually need.
Keep card data out of your systems entirely
The cheapest way to handle card security is to never touch a card number. Modern providers offer hosted payment pages and embedded card fields that send card details straight from the customer's browser or phone to the provider. Your servers receive a token that represents the card, never the number itself. That keeps most of your system out of the scope of the card industry's security standard, PCI DSS, and turns a heavy compliance exercise into a much lighter one.
Once a single server, log file or support tool sees a raw card number, that system and everything connected to it comes into scope. The usual leaks are accidental: a debug log that prints a request body, a support form where a customer types their card number, an analytics script on the checkout page. Review those paths deliberately.
The same thinking applies to the rest of the payment surface:
- Payout and refund actions in your admin tools need their own permissions, an audit log of who did what, and limits above which a second person must approve.
- Bank account details for payouts are sensitive data; collect them through the provider's onboarding where possible rather than storing them yourself.
- Rate limit payment endpoints and watch for patterns that look like card testing: many small attempts with different cards from one source.
A sensible build order for payment features
Payment features depend on each other, and building them in the wrong order means rework. The order that avoids it:
- The ledger and the payment states. Before the first real payment. Retrofitting a ledger onto live transactions means reconstructing history from provider reports, which is slow and never quite complete.
- Collecting payments through hosted fields, with idempotency keys and the unknown state handled from day one.
- Webhook handling and daily reconciliation, even if reconciliation is a report a person reads.
- Refunds, full and partial, written to the ledger as reversing entries.
- Payouts and held funds, if you pay anyone. Decide release conditions explicitly: on delivery, after a period, on a signed contract. Then make the release a state change your system controls, not a manual step in a dashboard.
- Wallets, multiple currencies and a second provider, each only once the product needs it.
Test the whole chain before launch with the provider's test mode: successful payments, declined cards, timeouts, duplicate webhooks, partial refunds and a payout that fails. Add an automated check that runs every night and confirms the ledger's basic promise, that the entries for every movement sum to zero and every wallet balance equals the sum of its entries. When that check fails, you want to know the same day, not when a customer writes in.
What this costs depends far more on how many of these layers you need than on screens: a checkout on top of one provider is a small piece of work, while payouts, held funds and a stored-value wallet each add a system of their own. Our fintech app development cost guide breaks that down, and our payment software development page shows the kinds of payment work we take on.
Whatever you build, the test is the same one an auditor or an investor's finance team will apply: pick any balance in your system and explain exactly how it got there. If the ledger can answer that in a query, the payment software is doing its job.
Adding payments, payouts or a wallet to your product? A Scoping Sprint ($2,300, two weeks) ends with your money flows, ledger accounts and build order made for your case, a prototype, and a fixed quote. Or just start a conversation.


