All field notes

How to Enable Subscriptions for Shopify Apps in India

Shopify's native subscriptions don't support Indian payment methods. Here's the architecture we built using Razorpay subscriptions, UPI recurring mandates, and Shopify's Admin API.

If you're running a Shopify store in India and want to offer subscriptions, you'll quickly hit a wall. Shopify's native subscription features rely on Shopify Payments, which doesn't support Indian payment methods like UPI autopay or Razorpay recurring billing. With UPI processing over 14 billion transactions per month, that's a big gap.

We solved this while building InvestYadnya, a financial advisory platform that needed one-time purchases and recurring subscriptions through Indian payment rails while keeping Shopify as the product catalog. Here's the architecture that works.

The core problem

Shopify handles product catalog, inventory, and fulfillment well. You probably don't want to rebuild all of that. But Shopify's checkout in India only supports one-time payments through Indian gateways. For subscriptions, you need:

  • Razorpay's subscription API for recurring billing
  • UPI recurring mandates (e-mandates) for autopay
  • A way to sync subscription payments back to Shopify for fulfillment
  • License management that tracks active subscription periods
Key insight

Don't fight Shopify's limitations. Use Shopify for what it's good at (catalog, pricing, discounts) and handle payments through Razorpay directly. Sync the results back to Shopify as "paid" orders.

The dual-API architecture

The solution uses two Shopify APIs for different purposes:

Storefront API: real-time pricing

Shopify's Storefront API is public-facing and handles cart creation with accurate pricing. We create a Storefront cart that applies automatic discounts, computes tax, and returns the exact amount to charge - including discount code validation. This is the source of truth for pricing. You never hardcode prices in your backend.

Admin API: order creation and fulfillment

After Razorpay confirms payment, we create a "mirror order" in Shopify via the Admin API. This order is created with financial_status: "paid" - bypassing Shopify's payment flow entirely. Shopify sees it as a paid order and triggers fulfillment (digital download emails, license keys, etc.).

The key detail: the order is created after payment confirmation, not before. This means Shopify never sees unpaid orders cluttering the dashboard, and fulfillment only triggers for verified payments.

Razorpay subscriptions for recurring billing

Razorpay's subscription API handles the recurring billing logic. Here's the flow:

Creating a subscription

When a user chooses a monthly or yearly plan, we create a Razorpay Plan (if one doesn't already exist for that amount/period) and then a Subscription against that plan. The checkout page renders Razorpay's payment modal, which supports UPI autopay, cards, and net banking.

UPI mandate limits - know the RBI rules

The RBI's e-mandate framework (originally issued August 2019, fully enforced October 2021) governs all recurring online payments in India. The per-transaction auto-debit limit without additional authentication was originally Rs 5,000, raised to Rs 15,000 in December 2021. The total_count parameter determines how many billing cycles the mandate covers. We set this to a high number (e.g., 20 for yearly = 20 years) rather than 0 (unlimited) because some banks reject unlimited mandates.

Handling the first charge vs renewals

This is where most implementations get tricky. Razorpay sends the same subscription.charged webhook for both the first payment and renewals. You need to distinguish between them:

  • First charge - Order status is still "payment pending." Create licenses, mark order as paid, sync to Shopify.
  • Renewal - Order is already paid. Extend the license period (update current_period_start and current_period_end).

The check is simple: if the order's status is already PAID, it's a renewal. If it's PAYMENT_PENDING, it's the first charge.

License management

Licenses are the bridge between payments and feature access. Each license tracks:

  • Which product variant the user paid for (mapped to Shopify variant IDs)
  • Current billing period - current_period_start and current_period_end
  • Auto-renew status - whether the subscription is active or cancelled

Permission checks query licenses at request time: "Does this user have an active license for this feature?" Active means the status is ACTIVE and current_period_end is in the future (or null for perpetual licenses). No background jobs, no caching - just a database query that's always current.

Preventing duplicate licenses

A database-level unique constraint on (user, shopify_variant_id) prevents a user from having two licenses for the same product. A check constraint ensures current_period_end >= current_period_start. These constraints live in the database, not in application code - they can't be bypassed by a bug.

Webhook idempotency

Razorpay webhooks can arrive multiple times. Network timeouts, gateway retries, or your server returning a 500 will all cause retries. If you process the same webhook twice, you might extend a subscription period twice or create duplicate Shopify orders.

The fix: store every webhook's event_id in a table with a unique constraint. Before processing, attempt to insert. If it's a duplicate, the database rejects it and you return 200 without processing. This is a three-line pattern that eliminates an entire category of payment bugs.

The complete flow

Here's how it all fits together:

  1. User selects products - Frontend sends variant IDs + optional discount code to the backend
  2. Backend gets pricing from Storefront API - Creates a Shopify cart, gets exact pricing with discounts applied
  3. Backend creates order + Razorpay entity - For one-time: Razorpay Order. For subscription: Razorpay Plan + Subscription
  4. User pays via Razorpay modal - Supports UPI, cards, net banking, UPI autopay for subscriptions
  5. Razorpay sends webhook - payment.captured for one-time, subscription.charged for recurring
  6. Backend processes webhook (idempotently) - Creates licenses, updates order status
  7. Backend creates Shopify mirror order - financial_status: "paid", triggers fulfillment
  8. Renewals - Same webhook, but extends license periods instead of creating new ones

What to watch out for

  • Don't store prices in your backend. Always fetch from Shopify's Storefront API. Prices change, discounts expire, and you don't want stale data.
  • Keep external API calls outside database transactions. If Shopify's API is slow, you don't want to hold a database lock for 10 seconds.
  • Log everything. Payment flows are the hardest thing to debug in production. Every webhook, every state transition, every API call should be logged with structured identifiers (order ID, payment ID, subscription ID).
  • Test the cancellation flow. Razorpay's subscription.cancelled webhook should set auto_renew=False on the license. The user keeps access until current_period_end, then it expires naturally.

The bottom line

Shopify subscriptions in India aren't impossible - they just require you to own the payment layer. Use Shopify for catalog and fulfillment, Razorpay for payments and subscriptions, and a Django backend to orchestrate the flow with proper idempotency and license management.

We built this for InvestYadnya and it's been running in production handling real subscription payments without issues. The patterns - dual Shopify APIs, webhook idempotency, license period management - apply to any Shopify store in India that needs recurring billing.

When you can't use the default path, build a better one. Just make sure the foundation is solid.

Building something like this?

No pitch, no pressure. We'll tell you honestly which option fits — even if it's not us.

Book a 30-min call More field notes