Integrating Payment Gateways in React Native — Lessons from the Trenches

Table of Contents

Payments are one of the trickiest parts of any mobile app. Over the past couple of years, I’ve worked with multiple payment systems — Razorpay, PhonePe, Apple/Google In-App Purchases, UPI AutoPay subscriptions, and vendor split payments. Here’s what I learned the hard way.

Why Payments Are Hard in Mobile

Unlike web, mobile apps have to deal with:

  • OS-level restrictions (especially iOS for in-app purchases)
  • Deep links and app switches (UPI apps like GPay, PhonePe, Paytm)
  • Async payment flows where success/failure comes via webhook, not response
  • Test mode vs live mode behaving very differently

Razorpay — The Workhorse

Razorpay is the go-to for Indian apps. The React Native SDK is solid but has some quirks.

Basic Setup

import RazorpayCheckout from 'react-native-razorpay';

const options = {
  description: 'Product Purchase',
  currency: 'INR',
  key: 'YOUR_KEY_ID',
  amount: 50000, // in paise (₹500)
  name: 'Your App',
  order_id: 'order_xxxx', // created from backend
  prefill: {
    email: 'user@example.com',
    contact: '9999999999',
    name: 'User Name',
  },
};

RazorpayCheckout.open(options)
  .then(data => console.log('Success:', data))
  .catch(err => console.log('Failed:', err));

The order_id trap 🚨

Always generate order_id fresh from your backend right before opening the checkout. If you reuse a stale order ID (from component mount for example), the payment silently fails. I hit this one hard.

UPI AutoPay — Subscriptions

Razorpay’s subscription flow for UPI AutoPay works differently from one-time payments. Instead of an order_id, you use a subscription_id:

const options = {
  subscription_id: 'sub_xxxx',
  description: 'Monthly Plan',
  currency: 'INR',
  key: 'YOUR_KEY_ID',
  name: 'Your App',
};

The critical thing here — the first charge is captured automatically, but subsequent charges go through a mandate. If Razorpay’s platform has an outage during the capture window, the payment gets debited from the user but not captured on your end. Always monitor your webhook for payment.captured vs payment.failed.

International Payments (EUR/USD)

International currencies require a separate gateway configuration in Razorpay dashboard. A common mistake is enabling international payments only for subscriptions but not for one-time orders, which results in a payment_risk_check_failed error. Make sure both pipelines are configured.

PhonePe — Direct UPI Integration

PhonePe’s SDK gives you a more native UPI experience. The flow works via deep links:

ls The tricky part is handling the app switch callback correctly on Android. The result doesn’t come back via a promise — it comes back through onActivityResult. Make sure you handle the back navigation state properly.

In-App Purchases (iOS & Android)

For subscriptions or digital content, Apple and Google mandate you use their own IAP systems — no Razorpay or PhonePe allowed.

I used react-native-iap for this. Key things to know:

  • Products must be configured in App Store Connect and Google Play Console first before you can fetch them in code
  • Receipt validation must happen server-side — never trust the client
  • On iOS, sandbox testing behaves differently from production (faster subscription renewals, etc.)
  • On Android, acknowledge the purchase within 3 days or Google auto-refunds it
import Iap from 'react-native-iap';

await Iap.initConnection();
const products = await Iap.getProducts({ skus: ['com.app.premium'] });
const purchase = await Iap.requestPurchase({ sku: 'com.app.premium' });
// → validate receipt on your server
// → then finishTransaction()

Vendor Split Payments — Razorpay Route

For marketplace apps where you collect from a buyer and split to a vendor, Razorpay’s Route feature handles this. The flow:

  1. Collect full payment from buyer (normal checkout)
  2. Create a transfer from the captured payment to the vendor’s linked account
// Backend (Node.js)
razorpay.payments.transfer('pay_xxxx', {
  transfers: [{
    account: 'acc_vendor123',
    amount: 45000, // ₹450 to vendor
    currency: 'INR',
  }]
});

This is great for gig platforms, delivery apps, or anything with sellers. The vendor gets a Razorpay linked account (like a mini bank account within Razorpay).

Key Takeaways

  • Never trust the SDK callback alone — always verify payment status via webhook on your backend
  • Test mode ≠ Live mode — always do at least one live test with a small amount before launch
  • UPI deep links are fragile — always handle the case where the UPI app isn’t installed
  • Keep order creation close to checkout open — stale order IDs cause silent failures
  • Log everything — payment failures are hard to reproduce, so instrument aggressively

Payments in mobile are frustrating but rewarding to get right. Happy to answer questions — reach me on LinkedIn or GitHub.