Payment is the one place in a system where a mistake costs money directly. So it is designed as if the connection will drop at the worst possible moment: it will.
Every step can fail — and at each one the system has to stay in a state you can reason about.
checkout.example.rs
Transaction for order #4821
LIVE
no differences
A double click
one transaction
The amount is tampered with
signature valid
The customer closed the tab
callback received
The gateway did not answer
status pending
A refund
refunded
The daily reconciliation
no differences
WHAT CAN GO WRONG
01A double clickAn idempotency key makes retries safe: two requests create one payment, not two charges
02The amount is tampered withThe amount comes from the order on the server, not the form — the request signature is verified
03The customer closed the tabThe result arrives as a separate gateway notification — the order updates without the customer returning
04The gateway did not answerThe transaction is marked unknown and polled: its status is requested until it resolves
05A refundFull or partial, tied to the original transaction; a repeated request does not create a second refund
06The daily reconciliationThe statement is matched to transactions: differences are listed rather than hunted by hand
A FAMILIAR PICTURE
What happens when payments are wired up “by the manual”
The bare-minimum integration
The customer is charged twice because they pressed the button twice
The order status changes only if the customer returns to the site after paying
If the gateway times out, the transaction hangs in limbo forever
Refunds are issued in the bank's portal and the system never hears about it
Reconciliation is an accountant, a statement and an evening with a calculator
An engineered integration
A repeated request with the same key returns the same transaction instead of creating another
The result reaches the server directly and is processed independently of the browser
Unresolved transactions are polled automatically until they reach a final status
Refunds go through the system, so the order, the stock and the report all learn about it at once
Reconciliation is daily and automatic: the list of differences arrives in the morning
SCOPE OF WORK
What the payments work covers
Transaction model
Order and payment are separate entities with their own states; the transitions are written down.
Gateway integration
Initialisation, request signing, response handling and authenticated bank callbacks.
Idempotent operations
Idempotency keys, so a retried request never becomes a second charge.
Declines and timeouts
A clear message to the customer, the order preserved and automatic resolution of unclear statuses.
Refunds and voids
Full and partial operations from the admin panel, with permission checks and a record of who did it.
Reconciliation
Daily matching of transactions against the statement and a report of what does not add up.
Logging and observability
Every attempt is stored with the gateway's response; a rise in declines is visible at once, not through complaints.
Test scenarios
We test not only a successful payment but a decline, a drop, a double click and a refund.
TECHNOLOGY LANDSCAPE
What reliability is made of
The bank's formCard details are entered on the bank's side and never reach your serverRequest signingThe amount and parameters are signed — they cannot be altered in the browserIdempotency keysA repeated request returns the earlier result instead of a new transactionCallback queueGateway callbacks are processed one at a time and survive a server restartBackground pollingTransactions without a final status are polled until they resolveReconciliation reportA daily match against the statement and a list of mismatches
The acquiring contract and the rates are between you and the bank. We are responsible for money and orders agreeing, and for disputes being settled from the log rather than from memory.
QUESTIONS
What people usually ask
A customer was charged twice. How does that happen?
Usually a missing idempotency key: the user clicked twice, or the browser retried after a drop, and two transactions were created. The right fix is not hiding the button for a second but making a repeat with the same key return the earlier transaction.
What about payments stuck in limbo?
They appear when the gateway did not answer in time although the result already exists on its side. We mark such transactions as unresolved and poll their status until it is final. Without that mechanism the money is taken while the order stays unpaid, and someone sorts it out by hand.
Do we need to store card data for recurring charges?
No. For recurring charges the bank issues a token tied to the card: you store that instead of the details, and it works only for your transactions. The card numbers themselves never appear on your server, so the storage requirements do not apply to you.
How do we know the decline rate has risen?
From the transaction log and watching it. Every attempt is stored with the bank's response code, so you see not just “fewer payments” but why: insufficient funds, a limit rejection, a failed confirmation. Those are different problems with different fixes.
Can several payment methods run at once?
Yes, and it is the normal case: card, bank transfer and cash on delivery coexist. What matters is that each method has its own chain of statuses: cash on delivery is confirmed by the carrier's report rather than the bank statement, otherwise reconciliation stops adding up.