Skip to content

SaaS Product Development

UPI AutoPay integration, from a billing system

UPI AutoPay from the merchant's side of a live billing system: RBI's ₹15,000 line, who sends the pre-debit notice, and the webhook behind every charge.

Ridam Agrawal

Founder, Easinvy

Published
Read
9 min
On this page

In short

How do you integrate UPI AutoPay as a merchant?

For most merchants, through a payment gateway's subscriptions product. Easinvy Retail uses Razorpay Subscriptions: its checkout registers the mandate in the customer's UPI app and Razorpay charges each cycle. The merchant's own work is the webhook that records each charge, which has to survive retries and a renewal arriving twice.

Everything below is the autopay path of a live retail billing system, read on 27 September 2026. Easinvy Retail sells four plans on autopay: ₹499 a month, ₹1,399 for three months, ₹2,699 for six and ₹4,999 a year, all through Razorpay Subscriptions since 13 September 2026.

UPI AutoPay, from the merchant's side

A merchant rarely talks to UPI AutoPay directly. It goes through a payment gateway's subscriptions product, and the gateway talks to the rail.

Easinvy Retail uses Razorpay Subscriptions, which takes "Cards, UPI Autopay and Emandate". The shop owner picks a plan, Razorpay's checkout opens, and the owner chooses how to pay. If they choose UPI, the mandate is approved in their UPI app.

The product's code never decides anything by which method was chosen. It records the method Razorpay reports on each payment, for the admin panel, and nothing in the billing logic branches on it. That is deliberate: the mandate is Razorpay's to run, and what the product needs is the same whichever way the shop pays, which is to know when money arrived.

One limit belongs here rather than at the end. No UPI AutoPay mandate is known to have been authorised on the product. The one live subscription was a ₹1 monthly charge on 13 September 2026, and the notes kept on that test do not say which method it used.

The ₹15,000 line, and why every renewal here is under it

RBI's rules for recurring payments were consolidated this year into the Digital Payments – E-mandate Framework, 2026, issued 21 April 2026. Registering a mandate needs extra authentication: "The mandate shall be registered only after successful validation of additional factor of authentication (AFA)." So does the first charge. After that:

All recurring transactions may be authorised without AFA up to ₹15,000/- per transaction. Transactions above this amount shall be subject to AFA.

For UPI that extra factor is the PIN, and Google Pay's help says it in the app's terms: "If your recurring payment is less than ₹15,000, you need to enter your UPI PIN to set it up for the first time. If your recurring payment is more than ₹15,000, you'll need to enter your UPI PIN to approve each payment." Insurance premiums, mutual funds and card bills get a higher line, ₹1,00,000.

Every Easinvy plan is under ₹15,000 a cycle; the largest is ₹4,999 a year. A shop owner authorises once, and renewals go through without asking again. Razorpay states the same limits for its subscriptions: mandates up to ₹1,00,000, and frictionless debits up to ₹15,000.

The bank sends the pre-debit notice, not the merchant

Before every debit, the customer is warned. Who does the warning is something the pages ranking for this search disagree about. One tells merchants to send pre-debit notifications to customers themselves; another says they come from NPCI; a gateway's page implies the gateway sends them.

The RBI direction is not ambiguous:

An issuer shall send a pre-transaction notification to the customer, at least 24 hours prior to the actual charge / debit.

The issuer is the customer's bank. Google Pay's help agrees from the other side: "You'll get a notification 24 hours before any debit from the bank." The merchant does not send it.

This product's own code had it wrong too. The line that creates a subscription passes customer_notify: 1, with this comment:

// Razorpay emails and messages the shop about upcoming charges. That is
// the pre-debit notice RBI requires, and it is theirs to send.
customer_notify: 1,

Razorpay's documentation describes the flag differently: it "Indicates whether the communication to the customer would be handled by businesses or Razorpay". That is Razorpay's own emails, not the bank's regulated notice. The mistake has no consequence, because the obligation was never the merchant's and the flag defaults to on anyway. The comment is still wrong, and it is recorded here as it stands.

Plans, periods, and a value the API would not take

A subscription charges against a plan: an amount and how often. Razorpay's Create a Plan reference lists the possible values for period as daily, weekly, monthly, quarterly and yearly.

On 13 September 2026 the API refused quarterly. The commit that fixed it records the error: sending it "is refused with "Invalid argument for period passed", which the first press of the sync button reported." The three-month plan is now monthly with an interval of 3, and the six-month plan monthly with 6. Whether the reference or the API has changed since was not tested; doing so means creating a plan on a live account.

How a monthly plan with an interval of 3 appears in a customer's UPI app is not documented in anything read. Razorpay's plan reference only says that "For UPI, all undefined frequencies except daily, weekly, monthly, quarterly and yearly are considered as-presented."

Two more facts about plans shape the integration. Razorpay plans cannot be edited, so a price change is a new plan, and an existing mandate keeps the amount it was authorised for: in the product's own words, "a price change reaches a shop at its next decision, not mid-mandate." And a subscription needs a finite number of cycles. The product asks for ten years' worth, which is "until cancelled" for every practical purpose. Amounts go to Razorpay as whole paise, for the reasons in decimal vs float for money.

The webhook is where the integration lives

The mandate is Razorpay's. What the merchant owns is the webhook that hears about each charge, and it is the only part that decides whether a shop that paid gets what it paid for.

Razorpay's webhook best practices set the terms. Delivery is at least once: "Razorpay follows at-least-once delivery semantics." Anything but a 2xx is a failure, retried with backoff for 24 hours. Then: "If the webhooks continue to fail for 24 hours, the webhook is disabled." Order is not promised either: "you may not always receive the webhooks in order."

Duplicates are expected, and the tool for spotting them is the x-razorpay-event-id header, which "is unique per event and can help you determine the duplicity of a webhook event." The product keys on it, because, as its comment puts it, it is "stable across retries of the same event — unlike anything inside the payload."

The detail that matters is what counts as a duplicate. Razorpay's validation page says to check whether an event "is processed at your end." Processed, not received. The usual implementation records the event id with a unique constraint the moment it arrives, which records received. If the handler then fails, every retry is turned away as a duplicate, and the charge is never recorded. Hookdeck's general guide handles the same case by deleting the row on failure; this product keeps the row and leaves it unfinished instead:

.onConflictDoUpdate({
  target: [webhookEvents.provider, webhookEvents.eventId],
  // A no-op write, so the row and its processed_at come back either way.
  set: { eventType: input.eventType },
})
.returning({ id: webhookEvents.id, processedAt: webhookEvents.processedAt })

A row whose processed_at is empty is handed back for another attempt. The no-op set is there because ON CONFLICT DO NOTHING returns no row, and the handler needs the existing one to see whether it was finished.

The payment id is the real guarantee

The event table is not what stops a shop being credited twice. The comment on it says so plainly: "the unique index on payments.provider_payment_id is the actual guarantee, and correctness should not rest on the weaker of the two."

There are two writers. When a shop pays, the browser comes back from checkout and reports the payment, and the webhook reports it too. Both call the same function with the same payment, and the unique index on the payment id "decides which of them does the work." The browser path has no event id at all, which is why the guarantee cannot live in the event table.

The product's self-test has a check for exactly this, labelled "a fresh delivery of the same payment does nothing": a different event id carrying a payment already recorded. It is one of 53 checks in backend/src/db/tests/razorpay-selftest.ts, which signs its own webhook bodies and posts them through a running server. They were not run for this note.

A renewal arrives twice

Each renewal fires subscription.charged, which is the event that pays. It also fires payment.captured, the event an ordinary one-off payment uses. The webhook's comment:

A subscription's charge also arrives as payment.captured, carrying an invoice id and an order this database never created.

Handle that second event as an order, and every renewal is recorded as money for an order nobody made. The product ignores a payment.captured that carries an invoice_id and lets subscription.charged do the work. This was confirmed by the ₹1 live charge on 13 September 2026.

The subscription signature is the order signature backwards

When checkout finishes, it hands the browser a signature to verify. For a one-off order, Razorpay's integration guide gives hmac_sha256(order_id + "|" + razorpay_payment_id, secret). For a subscription it gives hmac_sha256(razorpay_payment_id + "|" + subscription_id, secret).

The payment id moves from second place to first. Each page is right, neither mentions the other, and copying the order code for subscriptions rejects every real payment. The product's comment on its subscription check: "Note the order — it is the reverse of the order signature above, and getting it backwards rejects every real payment."

Take the period end from Razorpay

When a charge lands, the product needs to know when the paid period ends. It could compute that, and it deliberately does not. It takes Razorpay's current_end from the payload:

They are the ones who will charge on that date, and two clocks that disagree by a day is a shop shown "paid until the 13th" being charged on the 12th.

It computes a date only when the payload carries none.

What this does not check

No UPI AutoPay mandate is known to have been authorised on the product, and the notes on the one live mandate do not say which method it used. So nothing here says what a UPI app shows for these plans, or how a three-month plan registers there.

Lifecycle events are not ordered. The product refuses to let a later event shorten a paid period, but it writes each event's status as it comes. A delayed subscription.halted landing after a subscription.charged would set a paying shop to suspended until the next event. That comes from reading the code and Razorpay's sentence on ordering; it has not been observed.

The webhook checks one secret. Razorpay's validation page says to "use the old secret for webhook signature validation while retrying older requests" after a rotation, which this code would not do. And when no webhook secret is set, the handler answers 503 so that "A retryable status keeps the events queued until the secret is set." After 24 hours that is not true; the webhook is disabled. Its response time has never been measured against Razorpay's five-second limit.

None of it is changed here. This is the integration as it runs, for a product we build and operate, Easinvy Retail. Building subscription billing that survives these cases is part of SaaS product development.

Questions people ask

How to setup AutoPay in UPI?
Approve the merchant's mandate request in your UPI app with your UPI PIN, which RBI requires when a mandate is registered. In Google Pay the request waits under Profile, Autopay, Pending. Easinvy Retail sends the shop owner to Razorpay's checkout, which creates that request.
Which UPI app supports AutoPay?
NPCI's own list of apps live on UPI AutoPay names BHIM and Paytm and carries no date. Google Pay documents AutoPay in its own help pages. Easinvy Retail does not choose the app; the shop owner approves the mandate in whichever UPI app they use.
What does it mean when UPI AutoPay is set to ₹15,000?
Under RBI's e-mandate framework, recurring debits up to ₹15,000 each go through without extra authentication once the mandate is registered; above that, each debit needs your UPI PIN. Every Easinvy Retail plan costs less than ₹15,000 a cycle.
Can I setup AutoPay in Gpay?
Yes. Google Pay's help describes approving a mandate from the Pending tab under Profile and Autopay, confirmed with your UPI PIN. Its help also says the bank notifies you 24 hours before a debit, and you can pause or cancel from that notification.

Sources

Ridam Agrawal

Founder and engineer at Easinvy. Writes up the parts of building and running a multi-tenant product that were expensive to get wrong. More about the studio →

Keep reading