Paytm Integration Skill

v1.0.0

Expert guide for integrating Paytm Payment Gateway APIs and SDKs into websites, mobile apps, and backend systems. Use this skill whenever the user is working...

0· 25· 1 versions· 0 current· 0 all-time· Updated 4h ago· MIT-0

Install

openclaw skills install paytm-integration-skill

Paytm Payment Gateway Integration Skill

Overview

Paytm Payment Gateway supports UPI, Credit/Debit Cards, Net Banking, and EMI. Supported integration variants in this skill: JS Checkout (web), Subscriptions / UPI Autopay, Payment Links, and Dynamic QR Codes — all backed by Server-to-Server APIs.


Key Concepts

ConceptDescription
MIDMerchant ID — unique identifier for your Paytm account
Merchant KeySecret key used to generate/verify checksums
txnTokenShort-lived token returned by Initiate Transaction API; used in all subsequent steps
CHECKSUMHASHHMAC-SHA256 signature generated with Merchant Key to authenticate API calls
ORDER_IDUnique merchant-generated identifier per transaction
callbackUrlURL where Paytm POSTs transaction result after payment

Environments

EnvironmentBase URL (newer MIDs — default)Legacy host
Staginghttps://securestage.paytmpayments.comhttps://securegw-stage.paytm.in
Productionhttps://secure.paytmpayments.comhttps://securegw.paytm.in

New merchants are provisioned on paytmpayments.com; older MIDs may still resolve only on paytm.in. Use whichever the dashboard shows for your MID — the two are not interchangeable per MID. Always build and test against staging first.


Core Integration Flow

⚡ Pick the right flow FIRST (read before generating any code)

Map the user's intent to one of the four flows before writing anything. Picking wrong produces code that "works" but solves the wrong problem — the most expensive class of bugs in this skill.

User says…FlowEndpointNeeds JS Checkout?Reference
"checkout page", "pay button on website", "one-time payment", "buy"PaymentPOST /theia/api/v1/initiateTransaction (requestType: "Payment")✅ YesSteps below + references/js-checkout.md
"subscription", "monthly", "weekly", "yearly", "recurring", "auto-debit", "autopay", "mandate", "renew every…", "membership", "plan"SubscriptionPOST /subscription/create (requestType: "NATIVE_SUBSCRIPTION")✅ Yes (for consent screen)references/subscriptions.mdMUST READ
"shareable link", "invoice link", "payment link via SMS / WhatsApp / email"Payment LinkPOST /link/create❌ No — Paytm hosts the pagereferences/payment-links.md
"QR code", "scan to pay", "in-store", "counter", "table-side", "print QR"Dynamic QRPOST /paymentservices/qr/create❌ No — render image, customer scans with their UPI appreferences/qr-codes.md

The steps below describe Payment + JS Checkout only. Do NOT extrapolate them to the other three flows — they have different endpoints, different request shapes, different validators. Load the matching reference file and follow its flow.

Critical mistakes that keep recurring:

  • Subscription: the endpoint is /subscription/create (NOT initiateTransaction). The requestType is NATIVE_SUBSCRIPTION (NOT SUBSCRIPTION, NOT Payment). Subscription fields are flat inside body — no subscriptionDetails wrapper.
  • Payment Link: identifier in fetch / update / resend / expire calls is linkId as a JSON number, NOT a string. Resend path is /link/resendNotification, NOT /link/resend.
  • Dynamic QR: posId is required (skipping it returns 400). amount is a string with two decimals.

Step 1 – Generate Checksum (Server-side)

Every API call requires a CHECKSUMHASH in the request header (as signature).

Use Paytm's official checksum library — available for Java, PHP, Python, Node.js, .NET, Go:

  • Docs: https://www.paytmpayments.com/docs/checksum/
  • GitHub: https://github.com/Paytm-Payments
# Python example
from paytmchecksum import PaytmChecksum
checksum = PaytmChecksum.generateSignature(json.dumps(body), MERCHANT_KEY)
// Java example
String checksum = PaytmChecksum.generateSignature(body.toString(), MERCHANT_KEY);

Verify response checksum (server-side, before trusting any payment response):

is_valid = PaytmChecksum.verifySignature(response_body, MERCHANT_KEY, checksumhash)

Step 2 – Initiate Transaction API

Called server-side to get a txnToken before rendering the payment UI.

Endpoint:

POST {BASE_URL}/theia/api/v1/initiateTransaction?mid={MID}&orderId={ORDER_ID}

Request body for one-time payment (all top-level body fields shown are required):

{
  "head": { "signature": "<CHECKSUMHASH over JSON.stringify(body)>" },
  "body": {
    "requestType": "Payment",
    "mid": "YOUR_MID",
    "websiteName": "YOUR_WEBSITE_NAME",
    "orderId": "ORD_ABC123",
    "callbackUrl": "https://yoursite.com/paytm/callback",
    "txnAmount": { "value": "1.00", "currency": "INR" },
    "userInfo": { "custId": "CUST_001", "mobile": "9999999999", "email": "buyer@example.com" }
  }
}

Building a subscription / recurring charge? Do NOT use this endpoint or this body. Subscriptions use a different endpoint (/subscription/create) with a different requestType ("NATIVE_SUBSCRIPTION") and flat subscription fields inside body (no subscriptionDetails wrapper). Full correct payload in references/subscriptions.md — read it before writing any code. Re-check the decision callout at the top of this section if you're unsure.

websiteName is per-MID (dashboard value, e.g. DEFAULT, WEBSTAGING, retail). channelId (WEB/WAP) and industryTypeId are usually inherited from the dashboard but can be overridden in the body. Response: body.txnToken — single-use, 15-min TTL.


Step 3 – Render Payment Page

Web – JS Checkout (browser-only — never paste into a Next.js / Remix / RSC server component; wrap in "use client" or guard with typeof window !== "undefined"):

<script src="{pgDomain}/merchantpgpui/checkoutjs/merchants/{MID}.js"
        type="application/javascript" crossorigin="anonymous"></script>
<script>
  window.Paytm.CheckoutJS.onLoad(function () {
    window.Paytm.CheckoutJS.init({
      root: "",
      flow: "DEFAULT",
      data: {
        orderId: "ORD_ABC123",
        token: "<txnToken>",
        tokenType: "TXN_TOKEN",
        amount: "1.00"
      },
      merchant: { redirect: false },
      handler: {
        notifyMerchant: function (e, d) { console.log(e, d); },
        transactionStatus: function (d) { window.Paytm.CheckoutJS.close(); }
      }
    }).then(function () { window.Paytm.CheckoutJS.invoke(); });
  });
</script>

Full reference + alternative config shape in references/js-checkout.md. Working copy-paste page at scripts/frontend/js-checkout.html.


Step 4 – Handle Callback

Paytm POSTs to your callbackUrl with:

ORDERID, MID, TXNID, TXNAMOUNT, PAYMENTMODE, STATUS, RESPCODE, RESPMSG, CHECKSUMHASH, ...

Always verify CHECKSUMHASH server-side before trusting the response. Never rely solely on callback — confirm via Transaction Status API (step 5).

Key status values:

  • TXN_SUCCESS — payment successful
  • TXN_FAILURE — payment failed
  • PENDING — awaiting bank confirmation

Step 5 – Transaction Status API (mandatory verification)

POST {BASE_URL}/v3/order/status
{
  "head": { "signature": "<CHECKSUMHASH>" },
  "body": { "mid": "YOUR_MID", "orderId": "ORDERID_98765" }
}

Treat this response as the final authoritative status. Call it server-to-server, not from the browser.


Refunds

Initiate Refund

POST {BASE_URL}/v2/refund/apply
{
  "head": { "signature": "<CHECKSUMHASH>" },
  "body": {
    "mid": "YOUR_MID",
    "txnType": "REFUND",
    "orderId": "ORDERID_98765",
    "txnId": "PAYTM_TXN_ID",
    "refId": "UNIQUE_REFUND_REF_ID",
    "refundAmount": "1.00"
  }
}

Refund Status

POST {BASE_URL}/v2/refund/status
{
  "head": { "signature": "<CHECKSUMHASH>" },
  "body": { "mid": "YOUR_MID", "orderId": "ORDERID_98765", "refId": "UNIQUE_REFUND_REF_ID" }
}

Server SDKs

Paytm provides server-side kits that wrap all major APIs + checksum generation:

LanguageInstall
JavaMaven: com.paytm.pg:merchant-sdk
PHPComposer: paytm/pg-php-sdk
Pythonpip install paytmchecksum
Node.jsnpm install paytmchecksum
.NETNuGet: Paytm.Checksum

SDK docs: https://www.paytmpayments.com/docs/server-sdk/


UPI Autopay / Subscriptions

For recurring payments use Paytm's Subscription (UPI Autopay) product. Different endpoint, different requestType, different field placement from one-time Payment — see references/subscriptions.md for the correct payload.

  • Create a mandate via POST /subscription/create with requestType: "NATIVE_SUBSCRIPTION" and subscription fields flat inside body (no subscriptionDetails wrapper).
  • The returned txnToken is consumed by JS Checkout exactly like a one-time payment, where the user approves the mandate.
  • Recurring debit / status / edit / cancel operations are out of scope for this skill — refer to live Paytm docs and validate paths before implementing.
  • Docs: https://www.paytmpayments.com/docs/api/initiate-subscription-api

Common API Response Codes

RESPCODEMeaning
01Success
227Checksum mismatch
330Invalid order ID
334Duplicate order ID
400Bad request / missing params
501System error (retry)

Test Credentials (Staging)

  • Cards: Use Paytm-provided test card numbers from the dashboard's Test Data section
  • UPI: Any UPI ID ending in @paytm for staging
  • Net Banking: Use the dashboard's listed test bank options

Dashboard: https://dashboard.paytmpayments.com → toggle Test Data mode


Quick Reference: API Endpoints

APIEndpoint
Initiate TransactionPOST /theia/api/v1/initiateTransaction
Fetch Payment OptionsPOST /theia/api/v2/fetchPaymentOptions
Process TransactionPOST /theia/api/v1/processTransaction
Transaction StatusPOST /v3/order/status
Initiate RefundPOST /v2/refund/apply
Refund StatusPOST /v2/refund/status
Create SubscriptionPOST /subscription/create

All endpoints prefixed with the environment base URL.


Pitfalls (read before shipping)

  1. websiteName must match the dashboard exactly. Wrong value typically makes initiateTransaction itself fail with body.resultInfo.resultStatus = "F" and a generic message; in some legacy MID configs it returns a token that then fails at the JS Checkout step. Either way, check the dashboard value first.
  2. txnAmount.value is a string with two decimals ("1.00"). 1, 1.0, 1.000 break things.
  3. orderId is single-use even on failure. Generate a new one for every retry. Charset: [A-Za-z0-9_@-], ≤ 50 chars.
  4. txnToken is single-use, 15-minute TTL. Don't cache or pre-fetch.
  5. Don't mix PG hosts. Staging MID + prod host (or vice versa) returns confusing 401/checksum errors.
  6. Browser callback ≠ webhook. Callback can be lost (popup blockers, network drop). Always reconfirm via Transaction Status API or the S2S webhook before fulfilling.
  7. Callback verification uses sorted form params minus CHECKSUMHASH — different shape from API checksum, and field names are UPPERCASE.
  8. JSON bytes used to sign must equal bytes sent. Don't re-serialize between hashing and POSTing.
  9. INR only for domestic Paytm PG.
  10. Popup blockers kill the modal flow on mobile; offer merchant.redirect: true as a fallback.
  11. Callback URL must be reachable from the user's browser AND match what your backend listens on. The reference backends default to http://localhost:{3001|5001|8080/paytm-backend} — when scaffolding a multi-service project (e.g. Next.js frontend on :3000 + separate backend), set PAYTM_CALLBACK_BASE (or PAYTM_CALLBACK_URL) to the backend's public URL, not the frontend's. Never hard-code localhost for production.
  12. Frontend fetch calls are browser-only. The reference HTML uses new URL("paytm/create-order", document.baseURI) which deliberately fails fast in SSR (no document). When using Next.js / RSC, isolate Paytm calls in client components or behind typeof window guards.

Symptom-driven debugging: references/troubleshooting.md.


Common Vibe-Coded Bugs (and how to avoid them)

These are real bugs Claude has produced when scaffolding Paytm integrations from prompts. Internalize the fixes — don't regenerate the broken patterns.

1. Hard-coded absolute paths to external certs / files

Symptom: Project ships with NODE_EXTRA_CA_CERTS=/Users/someone-else/certs/zscaler.crt (or similar) baked into .env or code. Works on author's machine, breaks on every other machine. Fix: Use project-relative paths for any cert / keystore / file the project owns. Place the cert inside the project (e.g. ./certs/zscaler.crt) and reference it relatively. Document in the README that corp-network users may need to point this at their local Zscaler/Netskope cert. For Node: NODE_EXTRA_CA_CERTS=./certs/zscaler.crt in .env, loaded via dotenv.

2. https://localhost in callback / dev URLs

Symptom: PAYTM_CALLBACK_URL=https://localhost:3001/paytm/callback — Paytm POSTs the callback, browser blocks the redirect because there's no SSL on localhost. Payment "succeeds" silently with no callback. Fix: Use http://localhost:3001 for local dev. Reserve https:// for deployed environments where TLS is real. The reference backends already default to http://localhost:{port} — don't override unless you've actually set up local SSL (mkcert, Caddy, etc.).

3. ❗ CheckoutJS.onLoad() wrapped inside a button click handler

This is the most common Paytm bug Claude generates. It looks correct but never fires.

Broken pattern (do not generate):

button.addEventListener("click", function () {
  fetch("/paytm/create-order", ...)
    .then(function (data) {
      window.Paytm.CheckoutJS.onLoad(function () {        // ❌ already fired
        window.Paytm.CheckoutJS.init(config).then(...);
      });
    });
});

CheckoutJS.onLoad(cb) fires exactly once, when the merchant CheckoutJS script finishes loading — which happens shortly after page load, long before the user clicks "Pay". By click time, onLoad has already fired and your callback never runs. The payment modal silently fails to open.

Correct pattern:

// Page-load level: enable the Pay button only once CheckoutJS is ready.
window.Paytm.CheckoutJS.onLoad(function () {
  payBtn.disabled = false;                                // or whatever signals readiness
});

// Click handler: CheckoutJS is already loaded, call init/invoke directly.
button.addEventListener("click", function () {
  fetch("/paytm/create-order", ...)
    .then(function (data) {
      var config = { /* ... */ };
      return window.Paytm.CheckoutJS.init(config).then(function () {
        window.Paytm.CheckoutJS.invoke();
      });
    });
});

The reference frontends in scripts/frontend/js-checkout.html and scripts/backend-*/public/checkout.html follow this pattern and include an explicit comment warning against the broken one.

4. Missing transactionStatus / notifyMerchant handlers

Symptom: Payment completes (or fails, or is cancelled) and the page just sits there. No success message, no failure message, no UI update. User reloads, gets confused, may double-pay. Fix: Always wire up both handlers in the init config:

handler: {
  notifyMerchant: function (eventName, data) {
    if (eventName === "APP_CLOSED")     setStatus("Payment cancelled.");
    if (eventName === "SESSION_EXPIRED") setStatus("Session expired. Retry.");
  },
  transactionStatus: function (data) {
    // data.STATUS: TXN_SUCCESS / TXN_FAILURE / PENDING
    if (data.STATUS === "TXN_SUCCESS") setStatus("Payment successful.");
    else if (data.STATUS === "PENDING") setStatus("Payment pending — we'll confirm shortly.");
    else                                setStatus("Payment failed: " + data.RESPMSG);
    window.Paytm.CheckoutJS.close();
    // ALWAYS reconfirm server-side via /paytm/order-status before fulfilling.
  },
},

transactionStatus is the user-facing status. notifyMerchant covers the lifecycle events (popup closed, session expired) where transactionStatus doesn't fire. Without these, the UI is silent and the user is stuck.

5. Do NOT render debug logs / status dumps on the user-facing screen

Symptom: The page shows raw event payloads, JSON.stringify(data) blobs, console.log mirrored into a <pre> tag, or a "Status: …" debug strip on the production checkout page. Looks unprofessional, leaks internal field names, and confuses real users.

Rule: When generating production-grade UI code, never add an on-screen logger / status panel / debug <pre> block. Use console.log / console.warn / console.error for developer visibility — that's what DevTools is for. The user-facing UI should show only clean, customer-readable messages:

  • "Payment successful"
  • "Payment failed — please try again"
  • "Payment cancelled"
  • "Payment pending — we'll confirm shortly"

The reference scripts/frontend/js-checkout.html includes a #status div for demo/learning purposes only. When scaffolding for a real product, drop that div and route diagnostics to console.* instead. No alert() either — use a proper toast / banner / modal in the host app's design system.

6. Merchant key in .env must be wrapped in double quotes

Symptom: Checksum generation produces wrong signatures even though the key looks correct. Paytm responds with resultCode: 227 (checksum mismatch). Hours lost debugging.

Cause: Paytm Merchant Keys often contain #, @, !, $, or % characters. In .env files, an unquoted # is treated as a comment delimiter — everything after it is dropped. Other special chars can also be mis-parsed by some dotenv loaders.

Rule: Always wrap the Merchant Key in double quotes in .env:

# ❌ Wrong — any '#' in the key truncates the value
PAYTM_MERCHANT_KEY=ab#cd@1234XYZ

# ✅ Correct
PAYTM_MERCHANT_KEY="ab#cd@1234XYZ"

Same rule applies to any other secret with non-alphanumeric chars (DB passwords, API keys, etc.). When generating .env / .env.example files, always quote secrets — don't try to inspect the key and decide.

7. .env file conventions

Rules (apply to every generated .env / .env.example):

  • PAYTM_ENVIRONMENT is always the first variable — everything else derives from it.
  • Pre-fill staging values so the file works out of the box for development. Users replace with production values when going live.
  • Wrap every value in double quotes, not just secrets. Consistent and avoids edge cases (e.g. # in keys silently truncating).
  • Generic placeholdersYOUR_MID, not YOUR_STAGING_MID_HERE. The environment lives in PAYTM_ENVIRONMENT, never baked into placeholder text.
  • All mandatory keys at the top, comments / optional overrides in a later section — keep the active config block clean and scannable.

Canonical .env.example:

PAYTM_ENVIRONMENT="staging"
PAYTM_MID="YOUR_MID"
PAYTM_MERCHANT_KEY="YOUR_MERCHANT_KEY"
PAYTM_WEBSITE_NAME="YOUR_WEBSITE_NAME"
PAYTM_CALLBACK_BASE="http://localhost:3001"

# ---------------------------------------------------------------------------
# Defaults are pre-filled for staging. To go live:
#   1. Set PAYTM_ENVIRONMENT="production"
#   2. Replace MID / MERCHANT_KEY / WEBSITE_NAME with your live credentials
# Everything below is optional — leave commented unless you need to override.
# ---------------------------------------------------------------------------
# PAYTM_PG_DOMAIN=""               # auto-derived from PAYTM_ENVIRONMENT
# PAYTM_CALLBACK_URL=""            # auto-derived from PAYTM_CALLBACK_BASE
# PAYTM_STATUS_API_URL=""          # auto-derived from PAYTM_PG_DOMAIN
# NODE_EXTRA_CA_CERTS="./certs/zscaler.crt"   # corp networks (Zscaler/Netskope) only

8. ❗ Picked the wrong flow (Payment vs Subscription vs Link vs QR)

This is the single highest-impact bug in the whole skill. Picking the wrong flow produces code that runs but solves the wrong problem — silent, expensive, often only caught in production.

Failure modes seen in production testing:

  • "Gym subscription of ₹1/month" → generated one-time Payment with requestType: "Payment". Charges once, never recurs.
  • "Monthly SaaS billing" → generated requestType: "SUBSCRIPTION" against /initiateTransaction. Wrong endpoint AND wrong requestType — Paytm's subscription endpoint expects "NATIVE_SUBSCRIPTION".
  • "Send a payment link via WhatsApp for ₹500" → generated full JS Checkout HTML page. User wanted a shareable URL.
  • "QR code on the counter for customers to scan" → generated JS Checkout modal. User wanted a printable QR image.
  • "Generate a QR for ₹100" → omitted posId → HTTP 400 from Paytm.
  • "Fetch / expire a payment link" → sent linkId as a string → "invalid link id" response. Paytm expects a JSON number.

Rule — pick the flow BEFORE writing any code, by mapping prompt keywords:

Prompt cueFlowCode generates…
"subscription", "monthly", "weekly", "yearly", "recurring", "auto-debit", "autopay", "mandate", "renew", "membership"SubscriptionBackend: POST /subscription/create with requestType: "NATIVE_SUBSCRIPTION" and flat subscription fields inside body. Frontend: JS Checkout for the consent screen. → references/subscriptions.md
"payment link", "shareable link", "send link via SMS/WhatsApp/email", "invoice link"Payment LinkBackend: POST /link/create. No frontend — Paytm hosts the checkout page; you only share the returned shortUrl. → references/payment-links.md
"QR code", "scan to pay", "in-store", "counter", "table-side", "print QR"Dynamic QRBackend: POST /paymentservices/qr/create. No JS Checkout — render the returned image (base64 PNG) or qrData (UPI deep-link) on a screen / print it. → references/qr-codes.md
"checkout page", "pay button on website", "in-app payment", "one-time payment"JS Checkout (Payment)Backend: requestType: "Payment" + Initiate Transaction. Frontend: scripts/frontend/js-checkout.html pattern. → references/js-checkout.md

Crucially: Payment Link and Dynamic QR flows do NOT require JS Checkout at all — no merchant .js script, no window.Paytm.CheckoutJS. The customer pays on Paytm-hosted infrastructure (web link or UPI app). The merchant's only frontend job is to display the URL / QR image.

If the prompt is ambiguous (e.g. "accept ₹1 payments", "integrate Paytm"), ask one clarifying question before generating: "Is this a one-time payment, a recurring subscription, a shareable payment link, or a QR for in-store?"


Reference Files

Core flow + supported products

  • references/js-checkout.md — JS Checkout, non-SDK form POST, full callback field list, callback-vs-webhook
  • references/troubleshooting.md — symptom → cause → fix tree, expanded RESPCODE table, decision tree
  • references/subscriptions.md — UPI Autopay & card mandates, charge/edit/cancel, NPCI pre-notification rules
  • references/payment-links.md — FIXED / REUSABLE / OPEN links, fetch, expire, SMS dispatch
  • references/qr-codes.md — Dynamic QR (DQR) generation, status, reconciliation

Reference backends + frontend

  • scripts/backend-node/ — Express + paytmchecksum
  • scripts/backend-spring/ — Spring MVC + RestTemplate
  • scripts/backend-python/ — Flask + paytmchecksum
  • scripts/frontend/js-checkout.html — minimal copy-paste browser page

Docs Links

  • Developer Home: https://www.paytmpayments.com/docs/
  • Checksum Library: https://www.paytmpayments.com/docs/checksum/
  • Server SDK: https://www.paytmpayments.com/docs/server-sdk/
  • JS Checkout: https://www.paytmpayments.com/docs/jscheckout/
  • Subscriptions: https://business.paytm.com/docs/api/initiate-subscription-api/
  • Payment Links: https://business.paytm.com/docs/api/create-link-api/
  • Dynamic QR: https://business.paytm.com/docs/api/create-qr-code-api/
  • API Reference: https://www.paytmpayments.com/docs/api/initiate-transaction-api
  • Dashboard: https://dashboard.paytmpayments.com

Version tags

latestvk976k9av3fbbtghhhzmy5rnqq985xkcz