←Back to blog
Article

Recovering failed UPI payments without spamming your customers

Recovering failed UPI payments without spamming your customers
S

StriveBit

4 min readE-commerce

Recovering failed and abandoned payments without turning it into a spam campaign

Take a furniture retailer where a third of orders fail at the payment step. Razorpay reports a `payment_failed` webhook, the customer sees a vague error, and the order sits in `pending_payment` until someone manually calls. A team of two cannot call 200 people a day, so most of these orders just die.

The fix is not more reminders. It is making the retry effortless and the notifications sparse enough that they feel like help, not marketing.

We separate failures into two buckets. Soft failures — network timeouts, app crashes, UPI intent closed mid-flow — where the customer intended to pay. Hard failures — insufficient funds, declined card, invalid CVV — where the payment itself was rejected. The retry strategy differs.

For soft failures, the window is five minutes. The customer is still on the page or just left it. We regenerate the payment link with the same `order_id`, preserving cart contents, address, and applied coupons. No new order, no duplicate inventory hold. Razorpay's `retry` flag on the payment link handles the UPI deep-link freshness issue — the original VPA intent expires after ten minutes on some apps.

import requests

def regenerate_payment_link(order_id, amount_paise, customer_phone):
    resp = requests.post(
        "https://api.razorpay.com/v1/payment_links",
        auth=(RAZORPAY_KEY_ID, RAZORPAY_SECRET),
        json={
            "amount": amount_paise,
            "currency": "INR",
            "reference_id": order_id,
            "contact": customer_phone,
            "retry": {"enabled": True, "max_attempts": 3},
            "options": {"checkout": {"prefill": {"method": "upi"}}}
        }
    )
    return resp.json()["short_url"]

For hard failures, we wait an hour before the first message. Same link, same order, but we add the WhatsApp template with the failure reason stripped of jargon. "Your payment for ₹12,400 could not be completed. Tap to try again: [link]" works better than "Payment failed: INSUFFICIENT_FUNDS." Nobody knows what that means.

The second attempt is SMS, six hours later. The third, if we even do it, is the next morning. We cap at three touches across two channels over 24 hours. After that, we release the inventory hold and mark the order `abandoned`. In our experience, retries after 24 hours convert in the low single digits — not worth the customer's irritation.

One edge case to handle before going live: the customer pays twice. The original UPI intent was slow, not dead, and it settles after the regenerated link has already been paid. Both webhooks arrive with the same `reference_id`. The order should be marked paid on the first success and any later success for the same order should trigger an automatic refund, with a message to the customer explaining what happened. This is rare, but it is the kind of rare that produces an angry call, and the refund logic is a few lines if it exists from the start.

The mistake we see most often is firing a notification on every `payment.failed` webhook. Razorpay retries the charge up to three times on some instruments, and each retry fires a webhook. If you trigger a message on each event, the customer gets three SMSes in ninety seconds. Deduplicate on `order_id` and only send on the final `payment.failed` or after a five-minute quiet period.

We also avoid the "abandoned checkout" email flow that platforms like Shopify push by default. It sends to anyone who viewed a product, which for Indian D2C mostly hits people who were price-checking. The recovery rate looks decent in the dashboard but the unsubscribe rate and spam complaints cost you deliverability on the campaigns that matter.

Track recovery per bucket and per touch, not as one number. Soft failures recovered inside the five-minute window are almost free and tell you little. Hard failures recovered on the first WhatsApp message are where the design is doing its work. If the SMS at six hours recovers close to nothing, cut it and go to two touches. The numbers will differ by category and price point, and a furniture order at ₹12,400 behaves differently from a ₹400 accessory.

Set up this way, a retailer should expect a meaningful share of failed payments to recover through regenerated links alone, before any human intervention, and the call team to be working a short list of genuine follow-ups rather than every failure of the day. A short, single WhatsApp message with a working link is help. Three messages across two channels in an hour is a spam complaint waiting to happen.

←Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch