There's a specific kind of 2 a.m. debugging session that only payments can produce. Money is involved, so you can't shrug and ship a fix tomorrow. Logs are involved, so you're grepping through webhook payloads with one eye closed. We run subscription billing with UPI Autopay and card mandates in three production apps, and every lesson in this post was paid for with one of those nights.
The documentation for recurring payments makes it look like a weekend job. Create a mandate, charge it monthly, collect your revenue. If you're integrating UPI Autopay, eNACH or card mandates right now, here are the five places where the documentation and reality quietly part ways.
1. The first charge is its own beast
Our worst production surprise, and the most common one I see other teams hit. With UPI Autopay, mandate approval and first payment tend to feel like a single event. With card and eNACH mandates they're not. The mandate activates, everything looks green, and no money moves, because on those rails the first charge often has to be raised explicitly as a separate call.
We found out when subscriptions started activating without payments behind them. Users had access, the ledger had nothing. The fix was treating first-charge as a separate state machine per payment method, and refusing to mark any subscription active until money had verifiably landed. An active mandate is a promise, not a payment.
2. Timezones will bite you exactly once a day
Mandate schedules are date-sensitive. Our server generated schedule dates in UTC. The payment gateway thought in local time. For anything created late in the evening, the start date drifted across midnight, and the gateway would occasionally reject the mandate window or schedule a charge outside the permitted range. Intermittent, of course. The worst kind.
Now every piece of billing logic in our stack runs on one declared timezone, timestamps are formatted exactly as the gateway specifies, down to the offset notation, and we log both what we sent and what the gateway echoed back. When those two disagree, you want to know in minutes, not at month-end.
3. Webhooks arrive twice, late, or not at all
Plan for all three, because you'll get all three. Duplicate delivery is normal. Out-of-order delivery is normal. Silent non-delivery happens more often than any status page will admit.
Three rules keep this survivable. Verify the HMAC signature on every incoming webhook, always; an unauthenticated webhook endpoint is an open invitation to fake payment events. Make handlers idempotent, keyed on the event ID, so a duplicate does nothing instead of crediting a wallet twice. And run a reconciliation job that polls the gateway API for anything webhooks missed. I've come to think of webhooks as rumours and the API as the newspaper of record.
4. The payload in the docs is not the payload in production
We parsed webhook payloads based on the documentation examples. Real events arrived with the interesting fields nested a level or two deeper, varying by event type. Successful payments were parsed as unknowns and quietly dropped. No errors anywhere. Just missing revenue in the reports and a very confused founder, which was me.
The boring fix works: parse defensively, capture raw payloads for every event, and build your parser from events you've actually received rather than examples someone wrote for a docs page two years ago.
5. Mandates have a whole life you need to model
Charges fail for mundane reasons. Insufficient balance on retry day, bank downtime, or the user paused the mandate from their banking app without ever opening yours. If your data model only knows "paid" and "unpaid", subscribers drift into states you can't explain to support, and support can't explain to users.
We model the full lifecycle now: active, retrying, in grace period, paused, revoked, expired. Each state has its own user messaging and its own retry rules that respect what the rails actually allow. Half of good subscription engineering is admitting how many ways things can be partially broken.
The short version
Per-method first-charge handling, one timezone everywhere, signed and idempotent webhooks, daily reconciliation, a real mandate lifecycle, raw event logs. None of it is glamorous. All of it is the difference between "we integrated payments" and "our payments are boring", and boring is the highest compliment a billing system can earn.
These defaults are baked into every payment integration we ship, whether it's Cashfree, Razorpay or Stripe underneath. If your subscriptions are misbehaving in any of the five ways above, I promise it's not just you. It's the territory.
