Recurring and saved methods
Saving a method and charging it again works on rails that support mandates. On a rail without
mandate support, setup_future_usage, an explicit mandate_data block and a zero-amount setup
all complete with 200 succeeded and return mandate_id: null. Nothing is stored, and the later
off-session charge is refused with ERR_4088. Treat a missing mandate_id on the first payment
as the signal, not the status of that payment.
Your dashboard shows what is enabled for your account; ask us to enable a mandate-capable rail.
Save a payment method once, then charge it again without the customer re-entering anything. This powers two flows over the same API:
- One-click: a returning customer pays with a method they already saved.
- Off-session (recurring): you charge a saved method later with no customer present, for subscriptions, top-ups, or usage billing.
Both work by saving a method against a customer_id on the first payment, then
referencing the token TensorRail returns on later payments. You never handle or store
card data: the token is a TensorRail reference, not the underlying credential.
Saving and reuse are available for methods that support it (such as cards). Methods that are inherently single-use in a market are charged fresh each time; the API shape is the same, so you can write one integration and let the platform apply what each method supports.
1. Save a method on the first payment
Set setup_future_usage and attach a customer_id. Use off_session when you intend to
charge later with no customer present (subscriptions), or on_session when the customer
will return and confirm themselves (one-click).
setup_future_usage saves the method used by this payment, so a confirming call still has
to say how to pay. Omitting the payment method with confirm: true is refused with
ERR_2002: A payment token or payment method data or ctp service details is required.
- curl
- Node
- Python
curl -X POST https://api.tensorrail.com/payments \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "INR",
"customer_id": "cus_8x2Kp",
"setup_future_usage": "off_session",
"confirm": true,
"payment_method": "upi",
"payment_method_type": "upi_collect",
"payment_method_data": { "upi": { "upi_collect": { "vpa_id": "success@upi" } } },
"return_url": "https://yourshop.com/checkout/complete"
}'
const res = await fetch("https://api.tensorrail.com/payments", {
method: "POST",
headers: { "api-key": "rail_full_test_xxx", "Content-Type": "application/json" },
body: JSON.stringify({
amount: 50000,
currency: "INR",
customer_id: "cus_8x2Kp",
setup_future_usage: "off_session",
confirm: true,
payment_method: "upi",
payment_method_type: "upi_collect",
payment_method_data: { upi: { upi_collect: { vpa_id: "success@upi" } } },
return_url: "https://yourshop.com/checkout/complete",
}),
});
const payment = await res.json();
import requests
res = requests.post(
"https://api.tensorrail.com/payments",
headers={"api-key": "rail_full_test_xxx"},
json={
"amount": 50000,
"currency": "INR",
"customer_id": "cus_8x2Kp",
"setup_future_usage": "off_session",
"confirm": True,
"payment_method": "upi",
"payment_method_type": "upi_collect",
"payment_method_data": {"upi": {"upi_collect": {"vpa_id": "success@upi"}}},
"return_url": "https://yourshop.com/checkout/complete",
},
)
payment = res.json()
Once the payment succeeds, the response carries the token you store against your customer:
{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "succeeded",
"customer_id": "cus_8x2Kp",
"payment_method_id": "pm_9c1f7e4a2d6b4f8b8c1e3a5d",
"mandate_id": "man_2d6a4f8b8c1e3a5d9e7b2f10"
}
| Field | Store it? | Use it for |
|---|---|---|
payment_method_id | Yes | Charging the saved method again (one-click and off-session) |
mandate_id | If present | Charging under a mandate the customer authorised for recurring debits |
Keep payment_method_id (and mandate_id, when the method uses a mandate) on your
customer record. That is all you need to charge again.
2. Charge off-session (recurring)
To bill later with no customer present, create a payment that references the saved token
and set off_session. Confirm in the same call.
- curl
- Node
- Python
curl -X POST https://api.tensorrail.com/payments \
-H "api-key: rail_full_test_xxx" \
-H "Idempotency-Key: sub-cus_8x2Kp-2026-08" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "INR",
"customer_id": "cus_8x2Kp",
"recurring_details": { "type": "payment_method_id", "data": "pm_9c1f7e4a2d6b4f8b8c1e3a5d" },
"off_session": true,
"confirm": true
}'
const res = await fetch("https://api.tensorrail.com/payments", {
method: "POST",
headers: {
"api-key": "rail_full_test_xxx",
"Idempotency-Key": "sub-cus_8x2Kp-2026-08",
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 50000,
currency: "INR",
customer_id: "cus_8x2Kp",
recurring_details: { type: "payment_method_id", data: "pm_9c1f7e4a2d6b4f8b8c1e3a5d" },
off_session: true,
confirm: true,
}),
});
const charge = await res.json();
import requests
res = requests.post(
"https://api.tensorrail.com/payments",
headers={
"api-key": "rail_full_test_xxx",
"Idempotency-Key": "sub-cus_8x2Kp-2026-08",
},
json={
"amount": 50000,
"currency": "INR",
"customer_id": "cus_8x2Kp",
"recurring_details": {"type": "payment_method_id", "data": "pm_9c1f7e4a2d6b4f8b8c1e3a5d"},
"off_session": True,
"confirm": True,
},
)
charge = res.json()
Give each recurring charge a stable, period-based Idempotency-Key (for example one per
customer per billing month). If a retry fires for the same period, the duplicate is
rejected rather than double-charging.
If the method was saved under a mandate, reference mandate_id instead of
recurring_details; everything else about the call is the same.
3. One-click for returning customers
For a returning customer who is present, charge the payment_method_id you stored on
their record. Present your saved-method choice in your own UI, then confirm the payment
against the token: no card fields, no re-entry. The call is the same as an off-session
charge, without off_session (the customer is here to complete any step-up the method
requires):
{
"amount": 50000,
"currency": "INR",
"customer_id": "cus_8x2Kp",
"recurring_details": { "type": "payment_method_id", "data": "pm_9c1f7e4a2d6b4f8b8c1e3a5d" },
"confirm": true,
"return_url": "https://yourshop.com/checkout/complete"
}
If the method needs authentication (for example a 3-D Secure challenge), the response is
requires_customer_action with a next_action; forward the customer to it exactly as in
Accept a payment.
Lifecycle and webhooks
A saved method is backed by a mandate, which has its own lifecycle independent of any one payment:
Recurring charges emit the same events as any other payment (payment_succeeded,
payment_failed, and the intermediate states). Drive your subscription state from those
webhooks rather than the create response, so an off-session charge that completes
asynchronously is reflected correctly. See Handle webhooks.
Your server holds the schedule
Everything above is you charging a saved method on your own schedule: you hold the billing logic (the plan, the dates, the retries), and each charge is an ordinary payment with its own webhooks, refunds and disputes. That is how recurring billing works on TensorRail; the API has no separate subscription objects.
Security
- You store TensorRail tokens (
pm_…,man_…), never card numbers. The tokens are scoped to your account and are meaningless elsewhere. - Card details never reach your servers or your database: they stay with the card processor that captured them. A mandate stays with the provider that registered it, and a saved token works only with that processor or provider.
- Revoking a saved method or mandate stops future charges against it.
Next steps
- Accept a payment: the first-payment flow in full
- Handle webhooks: drive subscription state from events
- Idempotency: safe retries for recurring charges