In this article
A customer pays. The money leaves their account. Your system still says the order is pending, so they pay again, or they call. Somebody opens the gateway dashboard, finds the payment, and marks the order paid by hand. If that happens every week in your business, the gateway is not broken. The integration is treating payment as a single yes-or-no answer, and it is not one.
This guide explains why the gateway and your database disagree, and the eight things a business system has to handle so that they stop disagreeing: signatures, duplicates, ordering, pending states, refunds, reconciliation, audit trails and alerts. Where behaviour differs by gateway, it says so and names the provider's own documentation.
Why the gateway and your database disagree
A card or UPI payment is a conversation between the customer's bank, a payment network, the gateway and your server. Your system learns the outcome in two ways. The first is the redirect: the customer's browser comes back to your site with a result. The second is the webhook: the gateway's server calls yours directly.
Each can fail on its own. The customer closes the tab before the redirect, or their phone loses signal after approving the payment in a banking app. The webhook arrives while your server is restarting. The bank answers late, after the gateway has already told you the payment failed. None of this is unusual. Gateways document all of it, which is why their guidance is consistent on one point: build for events that are late, repeated and out of order.
A payment is a set of states, not a yes or no
Start by writing down the states a payment can be in for your business, and which ones allow you to hand over goods, activate a subscription or issue a receipt. The diagram below is a generic version. Every gateway has its own names for these moments.
- CreatedAn order exists. Nothing is paid.
- Customer actionOTP, 3-D Secure or a UPI approval.
- ProcessingThe bank has not answered yet.
- SucceededSafe to fulfil.
- SettledThe money reaches your bank.
- FailedDeclined, or timed out.
- AbandonedThe customer left the page.
- Authorised, not capturedFunds held, then released.
- Refund requestedBy staff or by rule.
- Refund pendingThe gateway is attempting it.
- RefundedConfirmed by the gateway.
- Refund failedThe money comes back to you.
- DisputedThe customer's bank has reversed it.
- Late successPaid after you marked it failed.
| Moment | Stripe PaymentIntent | Razorpay payment |
|---|---|---|
| Waiting for the customer | requires_payment_method, requires_confirmation, requires_action | created |
| Bank has not confirmed | processing | created, until it resolves |
| Funds held, not yet taken | requires_capture | authorized |
| Paid | succeeded | captured |
| Did not complete | canceled, or back to requires_payment_method after a failed attempt | failed |
| Refunded | A separate Refund object with its own status | refunded, with a separate refund entity |
Keep your own state names and map each gateway's states to them in one place in the code. If you add a second gateway later, or the first one renames a state, the change stays in that mapping.
The browser redirect is not proof of payment
The redirect is for the customer's screen. It can be missing, and a URL can be edited. Decide that an order is paid only from something your server has verified: a signed webhook, or a status your server fetched from the gateway's API with your secret key. Stripe's own guidance is to fulfil on server-side events such as payment_intent.succeeded, and for Checkout on checkout.session.completed together with checkout.session.async_payment_succeeded for payment methods that confirm later.
A good pattern is to do both. When the customer returns, show a page that asks your server for the current status. If the webhook has already arrived, the page shows success. If not, it shows that the payment is being confirmed and checks again. The customer sees progress, and the decision still rests on verified data.
Verify every webhook before you trust it
A webhook endpoint is a public URL. Anyone who finds it can post a message saying an order was paid. Gateways solve this by signing each message with a secret that only you and they hold. Your server recomputes the signature and rejects anything that does not match.
| Gateway | Header | How it is verified |
|---|---|---|
| Stripe | Stripe-Signature | HMAC-SHA256 over the timestamp and the raw body, keyed with the endpoint secret. Official libraries reject events older than five minutes by default. |
| Razorpay | X-Razorpay-Signature | HMAC-SHA256 over the raw body, keyed with the webhook secret. |
| PayPal | Transmission headers | Either a call to PayPal's verify-webhook-signature API, or local verification against PayPal's certificate. |
- Verify against the raw request body. Most frameworks parse JSON before your code runs, and a re-serialised body no longer matches the signature. This is the most common reason verification fails on a correct secret.
- Keep the timestamp check where the gateway provides one. It stops an old, genuine message from being replayed.
- Store the webhook secret like any other credential: outside the code, different for test and live, and rotated when staff with access leave.
- Reject and log a failed verification. Do not process the event with a warning.
Make every handler safe to run twice
Gateways deliver webhooks at least once, which means sometimes twice. Stripe and Razorpay both say so and both tell you to deduplicate. Stripe recommends recording the event IDs you have processed. Razorpay sends an x-razorpay-event-id header for the same purpose.
Idempotency means that running the same operation twice has the same result as running it once. In practice it takes two layers:
- Record each event ID in a table with a unique constraint, in the same database transaction as the work it triggers. A second delivery fails the constraint and is acknowledged without doing anything.
- Make the business step itself conditional. An order moves from pending to paid only if it is currently pending. A receipt number is issued only when that transition happens. Stock is reduced once.
The same idea applies in the other direction. When your server asks the gateway to create a charge or a refund and the network drops, you do not know whether it happened. Stripe accepts an Idempotency-Key header on such requests and returns the original result if the same key is sent again, with keys kept for at least 24 hours. An Idempotency-Key header has been proposed as an internet standard, but the draft expired in 2026 without becoming one, so each gateway defines its own behaviour. Read the one you use.
Expect late, repeated and out-of-order events
If your endpoint does not answer with a success code, the gateway tries again. How long it keeps trying depends on the provider.
| Gateway | Retries | Worth knowing |
|---|---|---|
| Stripe | Up to three days in live mode, with exponential backoff | Event order is not guaranteed. |
| Razorpay | For 24 hours, with exponential backoff | Your endpoint must respond within five seconds. A webhook that fails continuously for 24 hours is disabled and has to be re-enabled by hand. |
| PayPal | Up to 25 times over three days | After that the event is marked failed and can be resent manually. |
Three design rules follow from that table.
- Acknowledge first, work second. Verify the signature, store the event, return success, and do the real work from a queue. A handler that sends email and generates a PDF before answering will time out on a busy day, and the gateway will send the event again.
- Never let an older event move an order backwards. If a payment-failed event arrives after the payment-captured event for the same payment, the order stays paid. Compare against the current state, not against the order of arrival.
- When in doubt, ask the gateway. If an event refers to a payment you cannot place, fetch its current status from the API and act on that. Stripe's newer thin events are built around this: the event carries only an ID, and you fetch the latest object.
Pending is not failed
The most expensive mistake in a payment integration is treating no answer as a no. Banks sometimes answer late. Razorpay documents this as late authorisation: when the bank does not respond, the payment stays in the created state, is marked failed after ten minutes, and can still move to authorised if the bank confirms within the following three days. Stripe's processing status covers payment methods such as bank debits, where confirmation can take several days and the payment is not guaranteed in the meantime.
So a customer can be debited for an order your system has already cancelled. Decide in advance what happens then:
- If the order can still be fulfilled, honour the late payment and tell the customer.
- If it cannot, refund it automatically and tell the customer. Do not leave it for someone to notice.
- If the customer already paid again, keep one payment and refund the other. Link every payment attempt to its order so duplicates are visible.
Capture settings matter here too. With Razorpay, an authorised payment that is not captured within the configured window is refunded to the customer automatically. An integration that authorises and then forgets to capture produces orders that looked paid and were not.
Failed transactions in India
For Indian payment rails, the Reserve Bank of India's September 2019 circular on turnaround time sets deadlines for reversing a failed transaction and compensation of ₹100 a day when a deadline is missed. For a card payment at an online merchant, or a UPI payment to a merchant, where the customer is debited and the merchant never receives confirmation, the reversal is due within five days of the transaction. For a UPI or IMPS fund transfer where the beneficiary is not credited, it is one day. Those deadlines bind banks and payment operators. They also tell you how long a customer may reasonably wait, and what your support team should say.
Refunds have their own lifecycle
A refund is a second transaction with its own states, not a switch on the original payment. Stripe refunds can be pending, requires_action, succeeded, failed or canceled. Razorpay refunds are pending, processed or failed, and Razorpay puts a normal refund at five to seven working days to reach the customer.
- Record a refund as its own row, linked to the payment, with the amount, the reason, who requested it and the gateway's refund ID.
- Show staff and customers "refund initiated" until the gateway confirms. Mark it refunded on the gateway's event, not on the button click.
- Handle the failure event. A refund can fail after it was accepted, for example when the customer's account has closed, and the money returns to your balance. Somebody has to send it another way.
- Support partial refunds from the start. The sum of refunds can never exceed the amount captured.
Reconcile every day, from the settlement backwards
Webhooks keep your system up to date. Reconciliation proves it is right. It is a daily three-way comparison between what your system believes, what the gateway recorded, and what reached the bank.
Work backwards from the money. A settlement or payout is one bank credit that covers many payments, minus fees, taxes, refunds and adjustments. Both major gateways expose that breakdown: Stripe through balance transactions and a payout reconciliation report, Razorpay through a settlement reconciliation API that lists the payments, refunds and adjustments in each settlement with the fee, the tax and the bank reference. Match each line to an order, and list what does not match.
| What you find | What it usually means | What to do |
|---|---|---|
| Paid at the gateway, pending in your system | A missed or rejected webhook | Fetch the status, mark it paid, and find out why the event was lost |
| Paid in your system, absent at the gateway | A manual edit, or a trusted redirect | Treat as unpaid until proven, and review who changed it |
| Two payments for one order | The customer retried, or a late success | Refund one and tell the customer |
| Amount differs | A partial payment, a currency conversion or a fee | Correct the ledger entry; do not adjust the order to fit |
| Refunded at the gateway, order still fulfilled | A refund made in the dashboard, outside your system | Record the refund and reverse what was delivered if you can |
| Settled amount differs from the bank credit | Fees, taxes or an adjustment | Post each component to its own account |
A healthy system produces a short exceptions list every morning and an empty one most days. If the list is long, the cause is upstream, in one of the earlier sections.
Audit trails, alerts and a manual queue
Every payment problem ends with the same question: what did the gateway tell us, and when? Keep enough to answer it.
- Store every webhook as received, with its headers, the time, the verification result and what your system did with it.
- Log every status change on an order or payment with its cause: which event, which job, or which member of staff.
- Allow manual corrections, but require a reason and keep the previous value. Reconciliation will need them.
- Alert on what matters: no webhooks received for an unusual length of time, a rising verification failure rate, payments pending beyond your threshold, and unmatched lines after the daily reconciliation.
- Give the exceptions a queue with an owner. An automated system still needs a person for the small number of cases it cannot settle.
The test of a payment integration is not the successful payment. It is what happens to the one that is neither clearly paid nor clearly failed.
A payment reliability checklist
Where to start with an existing system
If your team already corrects payments by hand, start with the evidence. Take one month of settlements and reconcile them against your orders. The exceptions will show which of the sections above is causing the work, and that is usually one or two things. Fix those before rebuilding anything.
Techimpace builds and maintains payment integrations inside business software, including online fee collection in Academica ERP and GYMBIM. A payment integration review reads the webhook handlers, the state model, the refund path and the reconciliation process, and ends with a short list of fixes in priority order. It does not start with a rewrite.
Frequently asked questions
Why does the payment gateway show a payment as successful while my system shows it as pending?
Usually because the webhook that reports the success was missed, rejected or processed out of order, and the system relied on the browser redirect. Verify and store every webhook, check the payment status from your server when the customer returns, and reconcile daily to catch anything that slips through.
Is the redirect back to my website enough to confirm a payment?
No. The redirect can fail to arrive and can be tampered with. Confirm payment from a signed webhook or from a status your server fetches from the gateway's API.
What is idempotency in payment processing?
It means an operation can run more than once and still have the effect of running once. Gateways deliver webhooks at least once, so a handler has to recognise an event it has already processed and skip it, and a retried API request must not create a second charge or refund.
How long do payment gateways retry a failed webhook?
It depends on the gateway. At the time of writing, Stripe documents retries for up to three days in live mode, Razorpay for 24 hours, and PayPal up to 25 attempts over three days. Check the current documentation for the gateway you use.
What should happen when a customer is debited but the order fails?
Your system should detect it from a late success event or from reconciliation, then either honour the order or refund the payment automatically, and tell the customer. In India, the RBI's 2019 circular also sets deadlines for banks and operators to reverse failed transactions.
How often should a business reconcile online payments?
Daily. Match orders to gateway payments and to the settlement that reached the bank, and give unmatched items to a named person. Problems are much easier to resolve within a day than at month end.
Can Techimpace review an existing payment integration?
Yes. Techimpace builds and maintains payment integrations inside business software. A review covers the webhook handlers, the payment state model, refunds and reconciliation, and ends with a prioritised list of fixes.
Paritosh Bag is a software engineer and the Founder & CEO of Techimpace Innovations Pvt Ltd. He has been building business software since 2010 and has led Techimpace since founding it in 2013, working across PHP and Laravel, JavaScript, cloud infrastructure, payment systems and AI automation.