---
title: "Recurring INR with mandates"
description: "Charge Indian subscriptions off-session through Razorpay with netbanking e-mandates and card mandates: mandate limits, pre-debit notices and revoked mandates."
canonical_url: "https://twominutebilling.com/help/developers/recipes/india-mandates"
---
# 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.

<table>
<thead>
  <tr>
    <th>
      Rail
    </th>
    
    <th>
      Payment method on the gateway account
    </th>
    
    <th>
      Notes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Netbanking e-mandate
    </td>
    
    <td>
      <code>
        bank_transfer
      </code>
    </td>
    
    <td>
      INR only. Limit up to ₹10,00,000 per charge. Some banks confirm a new mandate only the next working day.
    </td>
  </tr>
  
  <tr>
    <td>
      Card
    </td>
    
    <td>
      <code>
        card
      </code>
    </td>
    
    <td>
      Razorpay must enable recurring payments on cards for your account.
    </td>
  </tr>
  
  <tr>
    <td>
      UPI
    </td>
    
    <td>
      <code>
        upi
      </code>
    </td>
    
    <td>
      INR only. Not yet supported for live billing. Limit up to ₹1,00,000 per charge.
    </td>
  </tr>
</tbody>
</table>

## 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](https://twominutebilling.com/help/developers/getting-started/quickstart).

```ts [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

<code-group>

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

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Sahyadri Softworks", "default_currency": "INR" }'

export BIZ=biz_…
```

</code-group>

## 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.

<warning title="Select the payment methods">

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.

</warning>

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

```http
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:

<code-group>

```ts [TypeScript]
await api("PATCH", `/v1/businesses/${biz.id}`, {
  mandate_config: { headroom_bps: 15000, rail_ceiling_cap_minor: 10000000 },
});
```

```bash [curl]
curl -X PATCH $TMBILLING_URL/v1/businesses/$BIZ \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "mandate_config": { "headroom_bps": 15000, "rail_ceiling_cap_minor": 10000000 } }'
```

</code-group>

```json [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.

<code-group>

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

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ravi@example.com",
    "first_name": "Ravi",
    "last_name": "Kumar",
    "phone": "+91 90000 00001",
    "currency": "INR"
  }'

export CUS=cus_…
```

</code-group>

## 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.

<code-group>

```ts [TypeScript]
const session = await tmb.paymentMethods.createPortalSession(biz.id, customer.id);
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/portal_sessions \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

</code-group>

```json [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](https://twominutebilling.com/help/developers/recipes/checkout-and-portal).

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`.

```http
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:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        rail
      </code>
    </td>
    
    <td>
      <code>
        upi
      </code>
      
      , <code>
        emandate
      </code>
      
       or <code>
        card
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      See below.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        max_amount_minor
      </code>
    </td>
    
    <td>
      The per-charge limit the bank registered. tmbilling refuses a larger debit before it calls Razorpay. <code>
        null
      </code>
      
       means the gateway did not report it, not "no limit".
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        currency
      </code>
    </td>
    
    <td>
      The limit's currency.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        expires_at
      </code>
    </td>
    
    <td>
      When the mandate ends, if it has an end.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reference
      </code>
    </td>
    
    <td>
      The mandate reference. It appears in the pre-debit notice.
    </td>
  </tr>
</tbody>
</table>

<table>
<thead>
  <tr>
    <th>
      <code>
        status
      </code>
    </th>
    
    <th>
      What it means for collection
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        initiated
      </code>
    </td>
    
    <td>
      Approved by the customer, not yet confirmed by the bank.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        confirmed
      </code>
      
      , <code>
        active
      </code>
    </td>
    
    <td>
      Live. Renewals are charged off-session.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        paused
      </code>
    </td>
    
    <td>
      Paused by the customer. The payment method is <code>
        invalid
      </code>
      
       until the mandate resumes, and then it is usable again.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rejected
      </code>
      
      , <code>
        cancelled
      </code>
    </td>
    
    <td>
      Dead. The payment method becomes <code>
        invalid
      </code>
      
       and is not charged again.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        spent
      </code>
    </td>
    
    <td>
      A single-use mandate that has been used.
    </td>
  </tr>
</tbody>
</table>

## 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.

<warning title="Renewals only">

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.

</warning>

## 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](https://twominutebilling.com/help/developers/recipes/failed-payments).
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.

<note title="Deleting a payment method">

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.

</note>

## 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

<table>
<thead>
  <tr>
    <th>
      What you see
    </th>
    
    <th>
      Why
    </th>
    
    <th>
      What to do
    </th>
  </tr>
</thead>

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

## Related

- [Subscription billing for India](https://twominutebilling.com/solutions/india)
- [Connect Razorpay in the dashboard](https://twominutebilling.com/help/guides/connect-a-payment-gateway)


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
