| name | paytm-subscriptions |
| description | Paytm UPI Autopay / Native Subscription flow for recurring charges (monthly / weekly / yearly / daily mandates, SIPs). Covers `POST /subscription/create` with `requestType: NATIVE_SUBSCRIPTION`, the flat-body field placement (no `subscriptionDetails` wrapper), required `head.clientId` / `channelId` / `signature`, the `traceId` query param, retry / grace rules, default value choices, and the most common errors (4001 grace > frequency, custId sanitization, etc.). Also covers the full post-consent recurring lifecycle (`subscription/checkStatus`, `renew`, `preNotify`, `cancel`, `v3/order/status`), the `/theia/` vs non-`/theia/` host split, and subscription webhook events (`SUBSCRIPTION_DEBIT` / `SUBSCRIPTION_CANCEL`) — see `references/recurring-lifecycle.md`. Load this skill for ANY recurring charge - "subscription", "monthly", "autopay", "mandate", "renew every…", "membership", "plan", "SIP". Do NOT load for one-time payments.
|
| triggers | ["subscription/create","subscription/checkStatus","subscription/renew","subscription/preNotify","subscription/cancel","v3/order/status","NATIVE_SUBSCRIPTION","NATIVE_MF_SIP","subscriptionFrequency","subscriptionPaymentMode","subscriptionId","subsId","SUBS_ID","subStatus","SUBSCRIPTION_DEBIT","SUBSCRIPTION_CANCEL","pre-notification","UPI Autopay","auto-debit","eMandate"] |
Paytm Subscriptions (UPI Autopay / Native Subscription)
Recurring charges use a different endpoint, different requestType, different field placement from one-time Payment. Picking the wrong flow is the most expensive mistake in this skill — a "monthly subscription" generated as a one-time Payment charges once and never recurs.
This skill is split across two files. SKILL.md (this file) gives the overview, defaults, and most common errors. references/REFERENCE.md contains the full field table, required fields per subscriptionAmountType (FIX vs VARIABLE), per-rail constraints (CC/DC vs UPI vs BANK_MANDATE), the IST date generation snippets per language, the charge / cancel / edit flows, and the full error code table — all NOT repeated here.
Do not generate any subscription code until you have read references/REFERENCE.md.
❗ Endpoint path differs by environment — pick the right one
| Environment | Full endpoint URL |
|---|
| Staging | POST https://securestage.paytmpayments.com/subscription/create?mid=...&orderId=...&traceId=... |
| Production | POST https://secure.paytmpayments.com/theia/api/v1/subscription/create?mid=...&orderId=...&traceId=... |
Notice the path prefix changes: production has /theia/api/v1/ before /subscription/create; staging does not. Using the staging path on production returns HTTP 404 / 501; using the production path on staging returns the same. This is unlike /theia/api/v1/initiateTransaction which uses the same path on both environments.
⚠️ create is the ONLY endpoint on /theia/. All the post-consent management APIs (checkStatus, renew, preNotify, preNotify/status, cancel, v3/order/status) live on the non-/theia/ host on BOTH environments — {PG_DOMAIN}/subscription/<op>. Reusing the create base URL for management calls on production returns 404 (SVE-003) or an HTML error page. This host split is the single biggest time-sink reported by integrators — see references/recurring-lifecycle.md for the full per-endpoint host table.
In code, derive the URL from PAYTM_ENVIRONMENT:
const SUBSCRIPTION_URL =
process.env.PAYTM_ENVIRONMENT === "production"
? `${PAYTM_PG_DOMAIN}/theia/api/v1/subscription/create`
: `${PAYTM_PG_DOMAIN}/subscription/create`;
Quick spec
| Value |
|---|
| Endpoint (staging) | POST {PAYTM_PG_DOMAIN}/subscription/create |
| Endpoint (production) | POST {PAYTM_PG_DOMAIN}/theia/api/v1/subscription/create |
| Query params | mid, orderId, traceId (all required) |
requestType | "NATIVE_SUBSCRIPTION" (or "NATIVE_MF_SIP" for SIPs) |
head | { clientId, channelId, signature } — all required |
| Subscription fields | flat inside body — no subscriptionDetails wrapper |
Critical defaults (use these unless the user overrides)
-
subscriptionPaymentMode: "UPI" — UPI Autopay is the dominant recurring rail. "UNKNOWN" can render an empty checkout ("no payment options") on certain production MIDs even when the subscription product is provisioned — so default to "UPI". Use a specific value ("CC" / "DC" / "BANK_MANDATE") only when restricting to that rail; only fall back to "UNKNOWN" if you've confirmed it renders rails on your MID.
-
txnAmount.value: "2.00" — minimum for CC/DC mandates.
-
subscriptionGraceDays: ALWAYS set this field — it is mandatory, omitting it returns "Grace days value is mandatory". The valid value depends on the cycle length and must be < the cycle in days (else 4001 Grace days cannot be greater than the frequency):
| Cycle | subscriptionFrequency + Unit | Cycle in days | Valid subscriptionGraceDays |
|---|
| Daily | "1", "DAY" | 1 | "0" (only valid value) |
| Every 2 days | "2", "DAY" | 2 | "0" or "1" |
| Weekly | "1", "WEEK" | 7 | "0" to "6" (default "1") |
| Monthly | "1", "MONTH" | ~30 | "0" to "3" for CC/DC; default "3" |
| Yearly | "1", "YEAR" | 365 | "0" to "3"; default "3" |
⚠️ For UPI Autopay (the default subscriptionPaymentMode: "UPI"), subscriptionFrequencyUnit accepts only "WEEK" / "MONTH" / "YEAR" — "DAY" is rejected with 4001. Use "1" + "WEEK" for weekly UPI mandates (not "7" + "DAY"). The "DAY" unit (incl. daily / every-N-days) is only valid on non-UPI rails.
-
subscriptionStartDate = today in IST (YYYY-MM-DD). Generate at request time using an IST-aware helper (see references/REFERENCE.md § rule 17 for per-language snippets). Do NOT use new Date().toISOString().slice(0, 10) in Node — it returns UTC, and between 00:00–05:30 IST every night UTC is still "yesterday" → Paytm rejects with 5028 subscription start in past.
-
subscriptionEnableRetry: "0" with subscriptionRetryCount omitted.
-
No renewalAmount field.
-
Both subscriptionFrequency (number) AND subscriptionFrequencyUnit (period) required.
Minimum body shape
{
"head": {
"clientId": "C11",
"channelId": "WEB",
"signature": "<CHECKSUMHASH over JSON.stringify(body)>"
},
"body": {
"requestType": "NATIVE_SUBSCRIPTION",
"mid": "YOUR_MID",
"websiteName": "WEBSTAGING",
"orderId": "SUB_001",
"callbackUrl": "https://yoursite.com/paytm/callback",
"txnAmount": { "value": "2.00", "currency": "INR" },
"userInfo": { "custId": "CUST_001" },
"subscriptionAmountType": "FIX",
"subscriptionFrequency": "1",
"subscriptionFrequencyUnit": "MONTH",
"subscriptionStartDate": "2026-05-09",
"subscriptionExpiryDate": "2027-05-09",
"subscriptionPaymentMode": "UPI",
"subscriptionGraceDays": "3",
"subscriptionEnableRetry": "0"
}
}
custId must be sanitized to [A-Za-z0-9_] (authoritative — strip everything else) — special chars cause 4002. Although Paytm prose lists @ ! $ . as "accepted", support varies by MID; normalizing to [A-Za-z0-9_] is the only reliable cross-MID rule. Persist the sanitized form.
After create: render JS Checkout for consent
The returned txnToken is consumed by JS Checkout exactly like a one-time payment, where the user approves the mandate. JS Checkout setup: load the js-checkout skill.
After consent: the recurring lifecycle (REQUIRED for a real integration)
Creation alone is not a working subscription. A production plugin must also detect cancellations, charge renewals, send pre-debit notifications, and verify callbacks. Full request/response shapes, per-endpoint head requirements, and the correct hosts are in references/recurring-lifecycle.md. The essentials:
| Operation | Endpoint (host = {PG_DOMAIN}, non-/theia/, both envs) | Purpose |
|---|
| Status check | POST /subscription/checkStatus | Detect cancel / expiry — the only reliable signal |
| Recurring debit | POST /subscription/renew | Charge one cycle (you schedule it) |
| Pre-debit notify | POST /subscription/preNotify (+ /preNotify/status) | NPCI 24h notice — you must call this; Paytm does not auto-handle it |
| Cancel | POST /subscription/cancel | Terminate a mandate |
| Per-charge status | POST /v3/order/status | Did this specific debit succeed |
Three gotchas worth memorizing:
- 🚨 A cancelled mandate returns
status: "REJECT". Only subStatus (MERCHANT_CANCELLED / USER_CANCELLED) reveals it was a cancellation — mapping on status alone wrongly reads it as TXN_FAILURE. Read subStatus first.
- The mandate ID has three names:
subscriptionId (create response) → subsId (checkStatus / order/status) → SUBS_ID (redirect/webhook POST). Read all three defensively.
SUBSCRIPTION_CANCEL webhook may never fire on your MID. checkStatus polling is the dependable fallback for cancellation detection — don't rely on the webhook alone.
Frequent errors
| Code | Meaning | Fix |
|---|
4001 | Grace days > frequency | Drop subscriptionGraceDays or set < cycle length |
4002 | Invalid custId / merchant param | Sanitize custId to [A-Za-z0-9_@-] |
5028 | Subscription start in past | subscriptionStartDate must be today or future |
Full table in references/REFERENCE.md.