Payments and payouts · from our own builds
Razorpay Subscriptions integration: plans, UPI mandates, webhooks and reconciliation
Integrating Razorpay Subscriptions takes plans created from code, a server-side checkout that authorises a mandate, a webhook handler that is signed, stored and safe to replay, and a reconcile job that pulls the truth from Razorpay when a webhook goes missing. We built it for countrydevs.com's paid tool tiers and for Storafleet's INR and USD plans, and most of our fixes were not at checkout but in plan changes on UPI mandates, lost webhooks and cancellations that never reached Razorpay.
- countrydevs.com tool tiers
- Storafleet
- Proton Fitness Academy
Updated 20 October 2026 · checked against our code and the vendors' documentation
Where have we built this?
- countrydevs.com tool tiersMonthly and annual paid tiers for our own tools, with prorated upgrades, 23 handled webhook events and reconcile-on-read.
- StorafleetRazorpay Subscriptions in INR for Indian billing addresses and USD for everyone else, with a gapless GST invoice series. Built and tested; as of 16 September 2026 no production customer had been charged on it.
- Proton Fitness AcademyOne-off course enrolments on Razorpay Orders, priced on the server and checked with an HMAC-SHA256 signature (orders, not subscriptions).
What are the numbers and limits to know?
- Standard fee: domestic cards, UPI, netbanking, wallets
- 2% per successful transaction, with 18% GST charged on the feesource, checked 20 October 2026
- Subscriptions on cards
- 0.9% + platform fees per transaction; UPI, NACH and e-mandate subscription pricing is on requestsource, checked 20 October 2026
- International cards
- Up to 3% per successful transactionsource, checked 20 October 2026
- Failed renewal (cards and UPI)
- Subscription goes pending, retried on T+1, T+2 and T+3, then haltedsource, checked 20 October 2026
- Authentication payment
- The plan amount for an immediate start; ₹5, refunded automatically, for a future startsource, checked 20 October 2026
- Domestic card mandates above ₹15,000
- Additional factor authentication on every debit; international cards are not covered by the rulesource, checked 20 October 2026
- Webhook delivery
- Retried with exponential backoff for 24 hours, then the webhook is disabled; respond within 5 secondssource, checked 20 October 2026
- Plan changes
- Refused for UPI- and e-mandate-authorised subscriptions; an immediate change needs a prorated difference of at least ₹0.50source, checked 20 October 2026
- Longest subscription
- 100 yearssource, checked 20 October 2026
How do you integrate Razorpay Subscriptions, step by step?
- 1
Create plans from code, one per tier, cycle and currency
A Razorpay plan's amount and period cannot be edited once it exists, so a price change means a new plan. On countrydevs.com an admin creates a plan and our code creates the matching Razorpay plan and stores its ID. On Storafleet a sync job creates one plan per price version, billing cycle and currency, skips free and custom-priced plans, saves after every create so a failure cannot leave an unrecorded plan on Razorpay, and leaves existing IDs alone so a re-run creates nothing.
- 2
Create the subscription on the server
The price comes from your own plan record, never from the browser. Set total_count to cover the plan's whole intended life, because it is the number of cycles Razorpay will charge; Storafleet asks for the longest Razorpay allows, 100 years. Put your app's name and your user ID in notes. A future start_at defers the first charge; Storafleet uses that to turn a customer's credit into free cycles. Refuse a second checkout while one is in flight, so a double click cannot create two mandates.
- 3
Verify the checkout response against the subscription
For subscriptions Razorpay signs razorpay_payment_id, a pipe, then the subscription ID, with your key secret: the reverse of the order_id|payment_id string used for orders, and the subscription ID must come from your server, not from Checkout. Copying the order formula would reject every genuine payment. After the signature, fetch the subscription and act on its status, authenticated or active, rather than on the payment amount: with a future start the authentication payment is a refunded ₹5, not the plan price.
- 4
Receive webhooks on the raw body, store them, then answer
Compute HMAC-SHA256 over the raw request bytes with the webhook secret and compare it in constant time. Save every signed event before doing any work and reply 200 inside Razorpay's 5-second window, then process it. On countrydevs.com a retry job replays failed events from the database at 1, 5 and 15 minutes, then 1 and 4 hours, and alerts an admin after the fifth failure. Razorpay documents the x-razorpay-event-id header as unique per event, which makes it the right key for spotting duplicates. Both apps key on it. countrydevs.com used to fall back to the payment or subscription ID, which made later events for the same payment look like repeats; since October 2026 it keys on the header and records each charge and refund once, whichever of its events arrives first.
- 5
Let one event write the money
On Storafleet's checkout, payment.authorized, payment.captured and order.paid arrived before any subscription event, carrying an order ID we had never stored, and payment.captured and order.paid fire again on every renewal. We made subscription.charged the only handler that writes the invoice, the ledger entry and the new period end, keyed on the charge's order ID, and let payment handlers ignore orders they do not know. The charged handler takes the currency from the payment itself, after a first charge was once booked in the wrong currency because the subscription record had not been updated yet.
- 6
Branch plan changes on the mandate type
Razorpay will not update a subscription authorised by UPI or e-mandate. On countrydevs.com an upgrade works out the unused part of the current cycle in paise, collects the difference as a one-off Razorpay order, and then moves the customer: in place for card mandates, or for UPI and every other method by cancelling the old subscription and creating a new one that starts when the paid period ends. The new tier applies as soon as the difference is captured. Razorpay's update page limits domestic-card subscriptions to offer changes, so our in-place card path still needs one run with a real domestic card in test mode.
- 7
Make every cancellation reach Razorpay
Call Razorpay's cancel API from every path that ends a paid subscription: the customer's cancel, an admin's cancel, and a refund that ends access. If the call fails, record it and retry it on the next billing run. Razorpay cannot undo a cancellation, so 'resume' on a cancelled subscription has to open a fresh checkout for the same plan; only a paused subscription resumes in place.
- 8
Reconcile against Razorpay, not only from webhooks
After four rounds of payment fixes on countrydevs.com we made Razorpay the source of truth and our database a cache of it. A billing page read pulls the subscription from Razorpay first, at most once every 5 seconds per user. Every subscribe, upgrade, downgrade, cancel or resume is followed by background reconcile passes at 2, 5, 10, 20, 45 and 90 seconds. Missing payments are rebuilt from the subscription's invoices, every difference is logged as a drift event, and a job at boot and every day checks the account's webhook subscriptions against the 23 events the code handles.
What went wrong, and how was it fixed?
Upgrades failed on UPI mandates, and the error read "[object Object]"
A tester paid the prorated difference to upgrade. Razorpay captured it, but our handler called subscriptions.update on a UPI-authorised subscription and got HTTP 400: "subscriptions cannot be updated when payment mode is upi". The event failed five times and was abandoned, the tier never changed, and the stored error was "[object Object]" because Razorpay's SDK errors carry error.description, not message. The path had been written for card mandates and had never met a UPI one.
Fix: We record the mandate method on each subscription and branch upgrades and scheduled downgrades on it: in place for cards, cancel-and-recreate for everything else. SDK errors are now serialised from error.description, so the real message reaches the log and the admin alert.
A paying customer saw the free tier
A UPI subscription was active at Razorpay, but on our side only payment.authorized and subscription.authenticated had been processed; payment.captured, subscription.charged and subscription.activated never were, and the authenticated handler did not touch the user's tier. The account showed free for at least 19 minutes while Razorpay's own API returned active.
Fix: The authenticated handler now sets the tier, every billing read reconciles against Razorpay first, and every plan action is followed by background reconcile passes. A webhook that never arrives no longer decides what the customer sees.
The safety net used a filter Razorpay does not accept
Our first reconcile service listed a customer's subscriptions with subscriptions.all({ customer_id }). Razorpay rejected the parameter with a 400 the first time it really ran, on the very account it was built to rescue. It had been checked against the SDK's method names, never against the live API's parameters.
Fix: Reconcile now fetches each subscription we hold by its ID. Our rule since: every Razorpay call is made once against the real test-mode API before it is merged.
Cancelling only changed our own database
On Storafleet, cancelling a monthly or annual plan set a flag locally and never called Razorpay, so Razorpay would have kept charging the card. Worse, the renewal handler cleared the cancel flag when the next charge arrived, re-subscribing the customer with a real GST invoice. Our own end-to-end tests had passed because they read local state only. Production had no Razorpay subscriptions yet, so no customer was charged.
Fix: One function now tells Razorpay to stop charging and is called from the customer's cancel, the admin's cancel and the refund that ends access. A failed call is recorded, alerted and retried daily, and a renewal charge on a cancelled subscription raises an alert instead of quietly re-activating it.
A webhook that had never processed an event
Storafleet's raw-body hook captured requests to /api/webhook while the route lived at /api/payment/webhook, and the JSON parser had already consumed the body, so the handler was left with "[object Object]" instead of the bytes Razorpay signed, and every event was answered 400. A payment captured at Razorpay where the browser never called our verify endpoint was money taken with nothing delivered.
Fix: The hook now captures the route's real path and the handler fails loudly if the raw body is missing. We tested it over the public internet with exactly that case: a signed captured-payment event with no verify call produced the invoice and the active subscription on its own, and replaying it produced no second invoice.
Verify and webhook both fulfilled the same order
Once the webhook worked, the browser's verify call and the webhook could fulfil one order at the same moment: two invoices, two GST numbers and the customer's credit drained twice.
Fix: Both paths were made to compare-and-set the order from created to paid, so only one could win, backed by a partial unique index on the invoice's Razorpay order ID. Replaying the same event produced exactly one invoice.
Reconcile marked a payment in progress as dead
Razorpay reports an order as attempted while the customer's bank is still working on the payment. Reconcile-on-read saw a pending upgrade order in that state and marked it failed mid-payment, so the upgrade never ran although the customer paid.
Fix: An order is only treated as dead once it is at least 5 minutes old and Razorpay shows it failed, expired, or attempted with nothing paid.
Invoice numbers with gaps, and a first invoice numbered 00000
GST invoice numbers must run without gaps. On Storafleet the first invoice of every financial year was numbered 00000, a number was reserved before the invoice was validated so a failure burned it, and the financial year was worked out in server-local time.
Fix: One counter per financial year, reserved with an atomic $inc only after validation, with the year computed in UTC and credit notes on their own series.
What should you check before going live?
- Every Razorpay API call has run once against the real test-mode API
- Webhook signature checked over the raw bytes, compared in constant time
- Every subscribed event has a handler, and code checks the dashboard list
- An upgrade tested on a UPI mandate, not only on a card
- Cancel tested by reading the subscription's status at Razorpay afterwards
- A deliberately dropped webhook is repaired by reconcile
- total_count covers the plan's whole intended life
- Live-mode webhook registered at the real host, not a tunnel
- Invoice numbers reserved after validation; financial year in UTC
Questions about Razorpay Subscriptions
Can subscribers pay for a Razorpay subscription with UPI?
Yes, UPI mandates renew like cards. The difference shows up on plan changes: Razorpay will not update a UPI-authorised subscription, so an upgrade means cancelling it and creating a new one that starts when the paid period ends. Razorpay quotes UPI subscription pricing on request instead of publishing it.
Does Razorpay prorate an upgrade in the middle of a billing cycle?
Only in some cases. An immediate update works out a prorated difference of at least ₹0.50, but it is refused for UPI and e-mandate subscriptions, and Razorpay's docs limit domestic-card subscriptions to offer changes. On countrydevs.com we calculate the unused part of the cycle in paise ourselves, collect the difference as a one-off order, and move the plan once that payment is captured.
What happens when a subscriber's renewal payment fails?
Razorpay marks the subscription pending and retries on each of the next three days; if the last retry fails it is halted. In Storafleet's test runs four failed attempts produced three subscription.pending events and one subscription.halted. A halted subscription keeps generating invoices that are never charged, so after the customer updates their card someone must decide whether to collect or write off what was missed.
Can a cancelled Razorpay subscription be resumed?
No. Razorpay has no call that reverses a cancellation, even one scheduled for the end of the cycle. A paused subscription can be resumed; a cancelled one needs a new checkout and a new mandate. Our resume endpoint tells the app to open a fresh checkout with the same plan and cycle already selected.
Do I still need webhooks if I reconcile with Razorpay's API?
Yes. Webhooks usually update the account within seconds, and reconciliation makes it correct when one is late or lost. On countrydevs.com a billing page read pulls the subscription from Razorpay first, and every plan action is followed by reconcile passes over the next 90 seconds, so a lost webhook no longer leaves a paying customer on the free tier.
Will a renewal above ₹15,000 go through without the customer?
Not on an Indian card. Razorpay's card-mandate rules allow silent debits up to ₹15,000 (RBI raised the limit to ₹1 lakh only for mutual funds, insurance premiums and credit card bills); above that the customer must complete additional authentication for every debit, and an ignored request fails like any other renewal. International cards are not covered, and Storafleet's annual INR business plan is above the limit, so we plan for its renewal to need the customer's approval once a year.
How do you test Razorpay subscription renewals before going live?
In test mode, the dashboard's page for a subscription has a "Charge this now" control that runs a renewal and sends real webhooks, and failed charges can be forced until the subscription halts. Not every test card supports recurring charges: one that passed authentication never renewed for us, and Razorpay support pointed us to 4012 8888 8888 1881. Test mode also accepted cancel_at_cycle_end and left our subscription active, so we check cancellations by reading the subscription's status afterwards.
Sources
- Razorpay pricing
- Razorpay Subscriptions: payment retries
- Razorpay Subscriptions: workflow
- Razorpay Subscriptions: integration guide
- Razorpay API: update a subscription
- Razorpay Subscriptions: update
- Razorpay API: create a subscription
- Razorpay: card mandates on Subscriptions (RBI rules)
- RBI: processing of e-mandates for recurring transactions (12 December 2023)
- Razorpay webhooks: best practices
- Razorpay webhooks: validate and test
Need Razorpay Subscriptions in your app?
We have built it in production code, including the parts that went wrong. Tell us what your app needs and we'll say what it takes.
Ask about this integrationRelated guides: One Razorpay account for several apps · RevenueCat in-app subscriptions · Stripe payments, refunds and payouts. Built for: fitness apps.