If you have ever integrated a payment gateway, you already know that the difficult part is usually not creating a “Pay Now” button.
The difficult part starts after the button is clicked.
What happens if the customer completes the payment but your server never receives the callback? What if the webhook arrives twice? What if the payment says successful but the amount doesn't match the order? What happens when a transaction remains pending for several minutes? And perhaps most importantly — how do you identify a transaction that technically looks valid but doesn't look normal for that particular customer?
These are the problems that matter when you are building a real payment system.
UPI has made digital payments extremely convenient in India. From a customer's perspective, the flow is straightforward: open a UPI application, select a bank account, authenticate the transaction, and the money moves.
From a developer's perspective, there is considerably more happening behind the scenes.
A typical merchant application does not simply connect directly to “UPI.” Instead, the merchant usually works with an appropriate payment gateway, PSP, bank, or other authorized payment participant. The merchant application communicates with that integration layer, while the underlying UPI ecosystem handles the transaction between the relevant banks and participants.
A simplified flow looks something like this:
Customer → Merchant Application → Payment Provider/PSP → UPI Ecosystem → Banks → Payment Provider → Merchant Backend
The exact flow depends on the payment method and provider, but the important point for developers is this:
Your application should treat payment processing as an asynchronous, state-driven system — not as a simple API request that returns SUCCESS or FAILED.
Understanding the UPI Payment Lifecycle
Let's take a normal ecommerce transaction.
Suppose a customer is purchasing a product for ₹2,500.
Your backend first creates an order:
Order ID: ORD-20261007-1001
Amount: ₹2500
Currency: INR
Status: PAYMENT_PENDINGYour payment service then creates the corresponding payment request through your payment provider.
The customer completes the payment using their UPI application.
At this point, your frontend might receive a response, but that response should not automatically be treated as the final source of truth.
A robust backend generally waits for a server-side confirmation from the payment provider and/or performs transaction verification before marking the order as paid.
The transaction might move through states such as:
CREATED
↓
PAYMENT_INITIATED
↓
PENDING
↓
SUCCESS
↓
RECONCILEDOr:
PAYMENT_INITIATED
↓
FAILEDThe important distinction is between payment status and order status.
A payment can be pending while the order remains unpaid.
A payment can be successful while your webhook is temporarily unavailable.
A webhook can be delivered twice.
A payment can even be successful while your internal database update fails.
This is why payment systems need idempotency and reconciliation.
Why Idempotency Matters
One of the most common mistakes I see in payment integrations is assuming that a webhook will arrive exactly once.
You should never make that assumption.
For example, your payment provider sends:
payment_id = PAY123
status = SUCCESS
amount = 2500Your webhook processes it and marks the order as paid.
Then the same webhook arrives again.
If your code simply executes:
UPDATE orders
SET status = 'PAID'
WHERE order_id = 'ORD-1001';you may not see an immediate problem.
But real payment systems often have additional side effects:
- Wallet balance updates
- Inventory deduction
- Invoice generation
- Subscription activation
- Loyalty points
- Email/SMS notifications
- Affiliate commissions
If those operations are not idempotent, the same payment event could trigger them multiple times.
A better approach is to maintain a unique payment transaction identifier and process each event safely.
For example:
payment_transactions
id
order_id
provider_transaction_id
amount
currency
status
webhook_event_id
created_at
updated_atThen enforce uniqueness where appropriate.
The rule is simple:
The same payment event should produce the same business result, even if you receive it multiple times.
Never Trust the Frontend Payment Result
This is one of the most important rules in payment development.
Your frontend might receive:
{
"status": "success"
}That does not mean your backend should immediately mark the order as paid.
The frontend is not your payment authority.
Your backend should verify the transaction through the appropriate server-side mechanism provided by your payment integration.
At minimum, your verification logic should validate things such as:
- Internal order ID
- Provider transaction ID
- Expected amount
- Currency
- Payment status
- Merchant/payment reference
- Transaction authenticity/signature where applicable
For example:
Expected Amount: ₹2,500
Received Amount: ₹2,500
Status: SUCCESS
Order: ORD-1001
Transaction: PAY123Only after the required verification succeeds should your business logic move the order to PAID.
Where Fraud Detection Comes In
A successful payment does not automatically mean the transaction is safe from a business-risk perspective.
This is where payment risk management becomes important.
Fraud detection is not about finding one magical rule that identifies every fraudulent transaction.
In production systems, it is usually a combination of:
Rules + transaction history + behavioural signals + device signals + velocity + risk scoring + monitoring
For example, imagine a customer normally makes payments between ₹500 and ₹3,000.
Suddenly, the same account starts generating:
₹75,000
₹80,000
₹90,000
₹85,000within a short period.
Every individual payment might technically be valid.
But collectively, the behaviour is unusual.
That is exactly the kind of pattern a risk engine should detect.
Signals a Risk Engine Can Monitor
Depending on the application and the data legitimately available to it, a risk engine can evaluate signals such as:
- Transaction amount
- Transaction frequency
- Failed payment frequency
- Account age
- Historical transaction behaviour
- Device changes
- Multiple accounts associated with the same device
- IP reputation
- Unusual geographic patterns
- New beneficiary/payment destination
- Sudden changes in transaction behaviour
- Repeated transactions within a short time
- Previous fraud/chargeback indicators
None of these signals should automatically mean “fraud.”
They are signals.
The system should combine them to calculate a risk level.
A Simple Risk Scoring Model
For example, you could start with an illustrative scoring system:
High-value transaction +25
Unusual transaction frequency +20
New device +15
Suspicious IP +20
Multiple accounts on device +20
Repeated failed attempts +10
----------------------------------------
Maximum 110Then define operational thresholds:
0–30 Low Risk
31–60 Medium Risk
61+ High RiskA low-risk transaction might continue normally.
A medium-risk transaction could require additional verification or review.
A high-risk transaction could be blocked, delayed, or sent for manual investigation depending on the business and payment flow.
This is only an example architecture. There is no universal “UPI fraud score” that every application should use.
Don't Start With Machine Learning
There is a tendency to immediately say:
“We need AI for fraud detection.”
In my experience, that is often the wrong starting point.
Before building an ML model, build a clean transaction data pipeline.
You need reliable information about:
- Who initiated the transaction
- What they purchased
- Amount
- Timestamp
- Payment state
- Device/session information
- Previous transactions
- Failed attempts
- Provider response
- Final settlement/reconciliation status
Without clean historical data, your machine-learning model is going to have a very difficult job.
A good first version can be a transparent rules-based risk engine.
Once you have enough quality historical data, you can introduce statistical or machine-learning models to improve detection.
Reconciliation: The Part Developers Often Forget
Payment processing does not end when the API says SUCCESS.
You also need reconciliation.
Imagine this situation:
Customer Bank → Payment successful
Payment Provider → Payment successful
Your Server → Timeout
Your Database → Payment pendingFrom your application's perspective, the payment appears pending.
From the customer's perspective, money has already been debited.
This is where reconciliation becomes critical.
A reconciliation process periodically compares your internal transaction records against the records provided by your payment partner.
For example:
Internal Transaction
PAY123
₹2500
PENDING
Provider Record
PAY123
₹2500
SUCCESSThe reconciliation worker can identify the mismatch and update your internal state.
This is one of the reasons production payment systems should have both:
Real-time webhook processing
and
Periodic reconciliation
You need both.
A Practical Backend Architecture
A payment system for a modern application can be separated into a few logical services:
┌─────────────────┐
│ Customer │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Web / Mobile App│
└────────┬────────┘
│
▼
┌─────────────────┐
│ Payment API │
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Order Service Risk Engine Payment Provider
│ │ │
▼ ▼ ▼
PostgreSQL Risk Data UPI/Banking
│
▼
Reconciliation WorkerThe exact architecture will vary with the size of the application, but separating these responsibilities makes the system considerably easier to maintain.
Security Should Be Designed Into the Payment Flow
Some basic practices are non-negotiable:
- Never store UPI PINs.
- Never log sensitive payment credentials.
- Validate webhook signatures/authentication according to your provider's specification.
- Use HTTPS everywhere.
- Keep payment credentials and API secrets outside source code.
- Implement idempotency.
- Validate transaction amounts server-side.
- Maintain immutable or carefully controlled transaction records.
- Keep detailed audit logs.
- Restrict access to payment administration.
- Monitor unusual transaction patterns.
- Separate test and production credentials.
- Implement proper retry handling.
- Reconcile transactions regularly.
And one important operational rule:
Do not make payment status changes silently.
Every important payment-state transition should be traceable.
For example:
PAYMENT_INITIATED
→ PAYMENT_PENDING
→ PAYMENT_SUCCESS
→ ORDER_PAID
→ RECONCILEDYour logs should make it possible to understand why each transition happened.
What Happens When Things Go Wrong?
Production payment systems are mostly about handling things that don't go according to the happy path.
You should design for:
Webhook timeout
Duplicate webhook
Delayed webhook
Provider timeout
Bank timeout
Payment pending
Payment success but order update failed
Order created but payment never started
Amount mismatch
Invalid transaction reference
Reconciliation mismatch
Suspicious transactionIf your architecture handles only:
SUCCESS
FAILEDit is probably not ready for serious production traffic.
The Bigger Picture
UPI solved an enormous usability problem: making digital payments extremely easy for customers.
For developers, however, the challenge is different.
The goal is not simply to “integrate UPI.”
The goal is to build a payment system that remains reliable when:
- thousands of transactions are happening simultaneously,
- APIs timeout,
- webhooks arrive late,
- events are duplicated,
- customers retry payments,
- databases temporarily fail,
- and some transactions are intentionally trying to abuse the system.
That requires thinking beyond the payment button.
A production-grade payment architecture should consider the entire lifecycle:
Payment initiation → verification → state management → risk evaluation → fraud detection → ledger → reconciliation → monitoring
At Leamsoft Pvt. Ltd., this is the kind of engineering approach we take when designing payment-enabled digital systems: not just getting the transaction to work, but designing the surrounding backend so that payments remain secure, traceable, reliable, and maintainable as the business grows.