Authorized, but never charged
What a payment outage changed about how I build subscriptions and entitlements.
A gateway outage left AutoPay subscriptions authorized but not captured. The screen said success. The provider, backend, and entitlement state disagreed.
What failed
I lead subscription and payment work on a React Native OTT platform at Mogi I/O. The system covers StoreKit, Razorpay, PhonePe, Tangerine, UPI AutoPay, restore flows, and vendor payouts.
The outage exposed a bad assumption: a finished checkout is not proof that money moved. A callback only tells us what the app saw. The provider and the backend still need to agree.
The rule I use now
Access is assigned only after the backend verifies the provider transaction.
A verified transaction can create one business effect, even if the webhook is delivered twice, late, or out of order.
What changed
- Model
authorizedandcapturedas separate states. - Deduplicate by the provider transaction identity.
- Let the server decide amount, currency, and eligibility.
- Refresh access from the backend when the app returns to foreground.
- Keep enough transaction context to reconcile later.
Smaller bugs with the same shape
The same mistake showed up elsewhere. Orders were created too early. UPI returns did not always mean payment success. A StoreKit listener lived on the checkout screen instead of the app lifecycle. Restore purchases used a different path from normal startup.
Fixing those made the system less dependent on one screen, one callback, or one happy path.
Takeaway
Payment work is state work. The system has to reach the same answer for a customer whether it sees first purchase, retry, duplicate webhook, restore, or manual support recovery.
I use the same discipline in Slope: StoreKit is the source of truth, transaction observation is long-lived, and entitlement is recalculated when the app becomes active.