Skip to main content

Server SDKs

The examples below use upi, not card

The SDKs are method-agnostic — only the method value matters. Cards are not enabled on any account today (see Drop-in SDK), so use a method your account actually has — your dashboard shows which those are.

Official, typed, server-side clients for the TensorRail API. They wrap the REST API with idempotency keys, retries with backoff, and typed errors so you can create payments, issue refunds, manage customers, handle disputes and mandates, manage saved payment methods, and verify webhooks without hand-rolling HTTP.

Both SDKs expose one sub-resource per object: client.payments, client.refunds, client.customers, client.disputes, client.mandates, client.paymentMethods (client.payment_methods in Python), client.subscriptions, and client.webhooks.

A ninth, client.payouts, is present in both packages but not part of the merchant API: every route under /payouts answers 404, which the SDKs surface as MoneyOutNotEnabledError. It is retained so existing code keeps compiling. Do not build against it: money-out is withdrawn platform-wide, and the payout_* event types exist in the platform's event vocabulary but are not delivered to a merchant endpoint today, so nothing reaches an integration either.

LanguagePackageRegistry
Node.js (18+)@tensorrail/node-sdknpm
Python (3.9+)tensorrailPyPI

Both SDKs share the same conventions:

  • Base URL: https://api.tensorrail.com (default).
  • Auth: your merchant secret API key (rail_full_*), sent on the api-key header by the SDK. Keep it server-side; never ship a secret key to a browser or mobile client.
  • Amounts are in minor units: the lowest denomination of the currency, e.g. 4999 = $49.99 USD, 999 = €9.99 EUR.
  • Built-in idempotency, retries, and typed errors. Creating a payment is idempotent: set your own paymentId (payment_id in Python) — or pass an idempotency key and the SDK derives a stable one — and a retried create returns the original payment instead of creating a duplicate. See Idempotency.

The three rules for every payment that carries a method

  1. paymentMethod / payment_method is the family ("upi"); paymentMethodType / payment_method_type is the specific method ("upi_intent"). A family on its own is rejected with 400 ERR_2001 Missing required param: payment_method_type.
  2. paymentMethodData / payment_method_data is a single-key map tagged by the family{ upi: { upi_intent: {} } }. A bare { card_number, … } object is rejected with 400 ERR_2002 Json deserialize error: invalid value: map, expected map with a single key. It is required whenever the type is set.
  3. Prefer upi_intent over upi_collect — see Accept a payment.

Give the payer their next step

A confirmed payment usually comes back requires_customer_action. That is not an error: it means the payer still has something to do, and next_action carries it.

// Node
if (payment.next_action) {
const target = payment.next_action.sdk_uri; // upi://pay?… — open on a phone
const qr = payment.next_action.image_data_url; // QR data: URL — show on desktop
const poll = payment.next_action.poll_config; // { delay_in_secs: 3, frequency: 60 }
}
# Python
if payment.next_action:
target = payment.next_action["sdk_uri"]
qr = payment.next_action["image_data_url"]
poll = payment.next_action["poll_config"]

Render what you are given rather than branching on next_action.type: other methods carry different fields, and new methods appear without an SDK release. Node types the documented fields and stays open for the rest; Python returns a plain dict for the same reason.

Timestamps

The payments API sends its creation time as created, not created_at. Python's Payment exposes created and keeps created_at as an alias for it. Refund and Customer really do send created_at.

Which SDK?

These are server-side SDKs: they use your secret key and never touch raw card data. To collect a card in the browser, use the client-side Drop-in SDK or a Hosted Checkout; the customer's card is tokenized there and your server confirms with the resulting token.


Node.js

Install

npm install @tensorrail/node-sdk

Quickstart

import { TensorRail } from "@tensorrail/node-sdk";

const client = new TensorRail(process.env.TENSORRAIL_API_KEY!);

// Create and confirm a payment (amount is in MINOR units): 200000 = ₹2000.00
const payment = await client.payments.create(
{
amount: 200000,
currency: "INR",
confirm: true,
paymentMethod: "upi", // the family
paymentMethodType: "upi_intent", // the specific method — REQUIRED
paymentMethodData: { upi: { upi_intent: {} } }, // single-key map, tagged
email: "buyer@example.com",
},
{ idempotencyKey: "order-abc-123" },
);
console.log(payment.payment_id, payment.status);
// → order-abc-123 requires_customer_action

// Hand the payer their next step — see "Give the payer their next step" above
console.log(payment.next_action?.sdk_uri); // upi://pay?… — open on a phone
console.log(payment.next_action?.image_data_url); // QR data: URL — show on desktop

// Retrieve
const fetched = await client.payments.retrieve(payment.payment_id);

// Create now, confirm later: the method is NOT carried over from create,
// so pass it to confirm.
const draft = await client.payments.create({ amount: 200000, currency: "INR" });
const confirmed = await client.payments.confirm(draft.payment_id, {
payment_method: "upi",
payment_method_type: "upi_intent",
payment_method_data: { upi: { upi_intent: {} } },
email: "buyer@example.com",
});

// Cancel a payment that has not been confirmed yet
await client.payments.cancel(draft.payment_id);

// Refund a SETTLED payment (amount in MINOR units; omit to refund in full).
// A payment still in requires_customer_action has taken no money yet.
// `settledPaymentId` is a payment that has actually taken money. The `payment`
// created above is still requires_customer_action, so refunding IT returns
// 422 ERR_3004 — substitute the id of a settled payment here.
const settledPaymentId = "pay_replace_with_a_settled_payment_id";
const refund = await client.refunds.create(
settledPaymentId,
{ amount: 1000, reason: "requested_by_customer" }, // ₹10.00
{ idempotencyKey: "refund-abc-123" },
);

// List refunds (filters) and payments (cursor pagination)
const refundPage = await client.refunds.list({ paymentId: payment.payment_id });
const paymentPage = await client.payments.list({ limit: 20, startingAfter: payment.payment_id });

// Customers: create and retrieve
const customer = await client.customers.create({
name: "Ada Lovelace",
email: "ada@example.com",
});
const sameCustomer = await client.customers.retrieve(customer.customer_id);

// Disputes: list, accept, or submit evidence
const disputes = await client.disputes.list({ limit: 20 });
await client.disputes.submitEvidence("dp_abc123", {
productDescription: "Digital subscription",
customerEmailAddress: "ada@example.com",
});

// Mandates and saved payment methods
const mandates = await client.mandates.list(customer.customer_id);
const methods = await client.paymentMethods.list(customer.customer_id);

Switch on webhook events with typed constants instead of raw strings:

import { WebhookEventType } from "@tensorrail/node-sdk";

if (event.event_type === WebhookEventType.PAYMENT_SUCCEEDED) {
// fulfill the order
}

Configuration

const client = new TensorRail("your-api-key", {
baseUrl: "https://api.tensorrail.com", // default
maxRetries: 3, // retries on 5xx / timeout / 429
timeout: 30000, // request timeout in ms
debug: false, // log method, URL, status, duration (API key masked)
});

Error handling

Failed requests raise typed errors that subclass TensorRailError, each carrying the canonical ERR_* code, the HTTP status, and the requestId for support. See Error catalog.

import {
TensorRail,
TensorRailError,
AuthenticationError,
ValidationError,
RateLimitError,
} from "@tensorrail/node-sdk";

try {
await client.payments.create({ amount: -1, currency: "USD" });
} catch (err) {
if (err instanceof ValidationError) {
console.error("Invalid request:", err.message, err.code);
} else if (err instanceof AuthenticationError) {
console.error("Bad API key:", err.httpStatus);
} else if (err instanceof RateLimitError) {
console.error("Rate limited, retry after:", err.retryAfter);
} else if (err instanceof TensorRailError) {
console.error(err.httpStatus, err.message, err.requestId);
}
}

Verify webhooks

client.webhooks.constructEvent() verifies the signature and returns the parsed event in one call; it handles the signature scheme for you (see Webhooks for the underlying header and algorithm). Verify against the raw request body: do not parse and re-serialize first, or the signature will not match.

import express from "express";
import { TensorRail, WebhookVerificationError } from "@tensorrail/node-sdk";

const client = new TensorRail(process.env.TENSORRAIL_API_KEY!);
const app = express();

// Capture the RAW body (required for signature verification).
app.post(
"/webhooks/tensorrail",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = client.webhooks.constructEvent(
req.body, // Buffer (raw bytes)
req.header("TensorRail-Signature") ?? "",
process.env.TENSORRAIL_WEBHOOK_SECRET!,
);
// event is the verified, parsed payload. Handle it, then ack fast.
res.sendStatus(200);
// ... process asynchronously
} catch (err) {
if (err instanceof WebhookVerificationError) {
res.sendStatus(400);
return;
}
throw err;
}
},
);

Python

Install

pip install tensorrail

Quickstart

from tensorrail import TensorRail

client = TensorRail("your-api-key") # base_url defaults to https://api.tensorrail.com

# Create and confirm a payment (amount is in MINOR units): 200000 = ₹2000.00
payment = client.payments.create(
amount=200000,
currency="INR",
confirm=True,
payment_method="upi", # the family
payment_method_type="upi_intent", # the specific method — REQUIRED
payment_method_data={"upi": {"upi_intent": {}}}, # single-key map, tagged
email="buyer@example.com",
idempotency_key="order-abc-123",
)
print(payment.payment_id, payment.status)
# → order-abc-123 requires_customer_action

# Hand the payer their next step — see "Give the payer their next step" above.
# next_action is present only while the payment needs the payer to do something,
# so guard it: a payment that settled without interaction has none.
if payment.next_action:
print(payment.next_action.get("sdk_uri")) # upi://pay?… — open on a phone
print(payment.next_action.get("image_data_url")) # QR data: URL — show on desktop

# Retrieve
fetched = client.payments.retrieve(payment.payment_id)

# Create now, confirm later: the method is NOT carried over from create,
# so pass it to confirm.
draft = client.payments.create(amount=200000, currency="INR")
confirmed = client.payments.confirm(
draft.payment_id,
payment_method="upi",
payment_method_type="upi_intent",
payment_method_data={"upi": {"upi_intent": {}}},
email="buyer@example.com",
)

# Cancel a payment that has not been confirmed yet
client.payments.cancel(draft.payment_id)

# Refund a SETTLED payment (amount in MINOR units; omit to refund in full)
# `settled_payment_id` is a payment that has actually taken money. The payment
# created above is still requires_customer_action, so refunding IT returns
# 422 ERR_3004 — substitute the id of a settled payment here.
settled_payment_id = "pay_replace_with_a_settled_payment_id"
refund = client.refunds.create(
settled_payment_id, # a payment that has actually taken money — see the note above
amount=1000, # ₹10.00
reason="requested_by_customer",
idempotency_key="refund-abc-123",
)

# List refunds (filters) and payments (cursor pagination)
refund_page = client.refunds.list(payment_id=payment.payment_id)
payment_page = client.payments.list(limit=20, starting_after=payment.payment_id)

# Customers: create and retrieve
customer = client.customers.create(name="Ada Lovelace", email="ada@example.com")
same_customer = client.customers.retrieve(customer.customer_id)

# Disputes: list, accept, or submit evidence
disputes = client.disputes.list(limit=20)
client.disputes.submit_evidence(
"dp_abc123",
product_description="Digital subscription",
customer_email_address="ada@example.com",
)

# Mandates and saved payment methods
mandates = client.mandates.list(customer.customer_id)
methods = client.payment_methods.list(customer.customer_id)

Switch on webhook events with typed constants instead of raw strings:

from tensorrail import WebhookEventType

if event["event_type"] == WebhookEventType.PAYMENT_SUCCEEDED:
... # fulfill the order

The client is also a context manager, which cleanly releases the underlying HTTP connection pool:

with TensorRail("your-api-key") as client:
payment = client.payments.create(amount=999, currency="EUR") # €9.99

Error handling

from tensorrail import (
TensorRail,
TensorRailError,
AuthenticationError,
ValidationError,
RateLimitError,
)

try:
client.payments.create(amount=-1, currency="USD")
except ValidationError as e:
print(f"Invalid request: {e} (code={e.code})")
except AuthenticationError as e:
print(f"Auth failed: {e} (HTTP {e.status_code})")
except RateLimitError as e:
print(f"Rate limited, retry after {e.retry_after}s")
except TensorRailError as e:
print(f"API error: {e} (HTTP {e.status_code}, request_id={e.request_id})")

Verify webhooks

client.webhooks.construct_event() verifies the signature and returns the parsed event. Verify against the raw request bytes. See Webhooks.

from flask import Flask, request, abort
from tensorrail import TensorRail, WebhookVerificationError

client = TensorRail("your-api-key")
app = Flask(__name__)

@app.post("/webhooks/tensorrail")
def handle_webhook():
signature = request.headers.get("TensorRail-Signature", "")
try:
event = client.webhooks.construct_event(
request.get_data(), # raw bytes
signature,
secret="your-webhook-secret",
)
except WebhookVerificationError:
abort(400)
# event is the verified, parsed payload. Handle it, then return 200 fast.
return "", 200