Recipes

Recurring INR with mandates

Charge Indian subscriptions off-session through Razorpay with netbanking e-mandates and card mandates: mandate limits, pre-debit notices and revoked mandates.

In India a renewal can only be charged without the customer present if they first approved a mandate: a standing permission, with a per-charge limit, registered at their bank. tmbilling registers the mandate through your Razorpay account, sizes its limit from the subscription, sends the pre-debit notice, and stops using a mandate the customer revokes.

RailPayment method on the gateway accountNotes
Netbanking e-mandatebank_transferINR only. Limit up to ₹10,00,000 per charge. Some banks confirm a new mandate only the next working day.
CardcardRazorpay must enable recurring payments on cards for your account.
UPIupiINR only. Not yet supported for live billing. Limit up to ₹1,00,000 per charge.

Before you begin

  • A Razorpay account. Use its test keys in test mode and its live keys in live mode.
  • A test API key and the client from the Quickstart.
setup.ts
import { Tmbilling } from "@two-minute-billing/sdk";

const tmb = new Tmbilling({
  apiKey: process.env.TMBILLING_API_KEY!,
  baseUrl: process.env.TMBILLING_URL,
});

// The client does not wrap every setting yet. This helper sends any request with your key.
async function api<T>(method: string, path: string, body?: unknown): Promise<T> {
  const res = await fetch(`${process.env.TMBILLING_URL}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.TMBILLING_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      ...(body === undefined ? {} : { "Content-Type": "application/json" }),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return (await res.json()) as T;
}

Create an INR business

const biz = await tmb.businesses.create({ name: "Sahyadri Softworks", default_currency: "INR" });

Connect Razorpay

In the dashboard, open Settings → Payment gateways for the business and add Razorpay with your Key ID and Key Secret. Under payment methods, select Netbanking (eMandate), cards, or both. tmbilling registers its webhook endpoint with Razorpay for you, or shows the URL and secret to add by hand.

tmbilling does not guess a rail. An INR mandate on a Razorpay account with no payment methods selected is refused. When you select several, the customer's options are tried in the order UPI, card, netbanking.

You can also connect the account over the API. This call needs real Razorpay keys, so it is not run on this page:

POST /v1/businesses/{biz}/gateway_accounts
Content-Type: application/json

{
  "gateway_key": "razorpay",
  "label": "Razorpay",
  "credentials": { "key_id": "rzp_test_…", "key_secret": "…" },
  "currencies": ["INR"],
  "method_types": ["upi", "bank_transfer"]
}

Size the mandate limit (optional)

The customer approves a per-charge limit, and the bank refuses any debit above it. tmbilling sizes the limit from the next renewal, tax and discounts included, plus 30% headroom for plan changes. It caps the limit at ₹1,00,000 so UPI stays available, unless the renewal itself is larger. With nothing to size from yet, it asks for ₹15,000.

To change the headroom or the cap for one business, set mandate_config. headroom_bps is basis points of the renewal, so 15000 asks for 1.5 times it:

await api("PATCH", `/v1/businesses/${biz.id}`, {
  mandate_config: { headroom_bps: 15000, rail_ceiling_cap_minor: 10000000 },
});
Response
{
  "object": "business",
  "id": "biz_XXXXXXXXXXXXXXXXXXXXXX01",
  "default_currency": "INR",
  "mandate_config": {
    "headroom_bps": 15000,
    "rail_ceiling_cap_minor": 10000000
  }
}

Send "mandate_config": null to go back to the defaults.

Create the customer with a phone number

Razorpay needs the customer's phone number to register a mandate.

const customer = await tmb.customers.create(biz.id, {
  email: "[email protected]",
  first_name: "Ravi",
  last_name: "Kumar",
  phone: "+91 90000 00001",
  currency: "INR",
});

Let the customer approve the mandate

Only the customer can approve a mandate, on their bank's site or with their card. The simplest way is the hosted customer portal: create a portal session and send the customer its link. They add a bank account or a card there, and the mandate comes back as a saved payment method.

const session = await tmb.paymentMethods.createPortalSession(biz.id, customer.id);
Response
{
  "object": "portal_session",
  "id": "psess_XXXXXXXXXXXXXXXXXXXXXX01",
  "status": "created",
  "token": "pstk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "url": "https://billing.example.com/portal/s/pstk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "access_expires_at": "2026-10-20T20:13:12.335Z",
  "expires_at": "2026-10-23T19:13:12.335Z"
}

url opens on your portal host. The link works once, and must be opened before access_expires_at, one hour after you create it. A new customer can also approve the mandate while they pay for their first invoice in hosted checkout.

To build the flow in your own page, start the setup over the API. It needs a connected Razorpay account, so it is not run on this page. Open Razorpay Checkout with the returned checkout options, then save the result with POST /v1/businesses/{biz}/customers/{cus}/payment_methods.

POST /v1/businesses/{biz}/customers/{cus}/payment_methods/setup
Content-Type: application/json

{
  "provider": "razorpay",
  "return_url": "https://example.com/billing/done",
  "cancel_url": "https://example.com/billing"
}

Read the mandate

A payment method that rides a mandate carries a mandate object:

FieldMeaning
railupi, emandate or card.
statusSee below.
max_amount_minorThe per-charge limit the bank registered. tmbilling refuses a larger debit before it calls Razorpay. null means the gateway did not report it, not "no limit".
currencyThe limit's currency.
expires_atWhen the mandate ends, if it has an end.
referenceThe mandate reference. It appears in the pre-debit notice.
statusWhat it means for collection
initiatedApproved by the customer, not yet confirmed by the bank.
confirmed, activeLive. Renewals are charged off-session.
pausedPaused by the customer. The payment method is invalid until the mandate resumes, and then it is usable again.
rejected, cancelledDead. The payment method becomes invalid and is not charged again.
spentA single-use mandate that has been used.

Subscribe the customer

Create the subscription as usual. With the mandate's payment method as the customer's default, each renewal is charged without the customer. UPI and netbanking debits return pending and settle when Razorpay reports the capture, usually within minutes.

Know the limits

  • ₹15,000 per debit. Above it, Indian rules make a card or UPI debit wait for the customer to authenticate it. A netbanking e-mandate does not, so use netbanking for plans priced above ₹15,000.
  • The limit cannot be raised in place. If a renewal grows past max_amount_minor (an upgrade, more seats, tax), the debit fails with mandate_ceiling_exceeded and is not retried. The customer must approve a new mandate. tmbilling warns you first: when you create a subscription whose renewal is above the mandate's limit, it emits payment_source_mandate_insufficient.

Pre-debit notices

The customer must be told before each mandate debit: the amount, the date and the mandate reference. tmbilling emits subscription_pre_debit_notification and sends the pre_debit_notification email up to two days before each renewal on a mandate. The amount is the renewal estimate, tax included. Metered usage is not in it, because it is not known yet.

The notice covers scheduled renewals. Dunning retries, one-time invoices and an immediate collection also debit the mandate, and tmbilling sends no notice for them.

Handle a revoked mandate

A customer can cancel or pause a mandate at any time in their UPI app or bank. Razorpay tells tmbilling by webhook, and that is the only notice there is. tmbilling then:

  1. Records the new mandate.status and marks the payment method invalid.
  2. Stops charging it. The next renewal falls back to the customer's backup payment method, or fails and enters dunning.
  3. Restores the payment method by itself if a paused mandate resumes.

Ask the customer for a new mandate with a fresh portal link. A cancelled mandate cannot be revived.

Razorpay does not let anyone delete an active mandate. Deleting the payment method in tmbilling removes it here and stops all charges, but the mandate can stay registered at the customer's bank until they cancel it.

Check it worked

  • The customer's payment method has mandate.status confirmed or active and a max_amount_minor above the renewal total.
  • The first renewal's transaction moves from pending to succeeded once Razorpay reports the capture.

Common errors

What you seeWhyWhat to do
Razorpay requires a phone number for recurring payments. Add a phone number to the customer. Then try to add a card again.The customer has no phone.Set phone on the customer.
The selected gateway account (…) does not allow these payment methods: …The method is not selected on the Razorpay account.Select it under Settings → Payment gateways.
A debit fails with mandate_ceiling_exceededThe charge is above the mandate's limit.Ask the customer to approve a new mandate.
A renewal stays pending above ₹15,000A card or UPI debit is waiting for the customer to authenticate it.Ask the customer to approve the debit, or move them to a netbanking e-mandate.
Copyright © 2026