Hosted checkout
Hosted checkout is the fastest way to launch. You create a payment on your server, then redirect the customer to a TensorRail-hosted payment page. TensorRail draws the payment screen, presents every method enabled for your account, runs each rail's specifics (redirects, in-app approvals, reference displays), and returns the customer to you. You write one create call and one redirect, and you never build or maintain a payment UI.
This is one of three integration paths onto the one platform: the account, balance, webhooks, and reporting are identical whichever path you use. The only difference is who draws the payment screen. Here, TensorRail does.
When to use it
- You want to be live in a day and never maintain a checkout UI.
- You want new methods and corridors to appear automatically, with zero code, as they are enabled for your account.
- You are fine handing the payment screen off to a TensorRail-hosted page.
If you need the payment experience fully inside your own product, use the direct API instead. If you want to share a checkout URL rather than redirect to it, use payment links.
The flow
- Create the payment server-side with your secret key. You get back
payment_id,client_secret, and astatus. - Redirect the customer to
pay.tensorrail.com, passing the payment'spayment_idandclient_secret, your browser-safe publishable key (pk), and the displayamount/currency. - The customer pays on the hosted page. TensorRail renders every enabled method and drives the rail the customer chooses.
- The customer returns to your
return_url, and the authoritative outcome arrives on your webhook.
Step 1: Create the payment
POST /payments with your secret key, server-side. amount and currency are required;
currency must be a corridor enabled for your account. Set return_url to where the customer
should land after paying.
- curl
- Node
- Python
curl -X POST https://api.tensorrail.com/payments \
-H "api-key: rail_full_test_xxx" \
-H "Idempotency-Key: 8f14b6b0-2e2b-4c1a-9d5e-7a3c1f0b9e21" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "INR",
"description": "Order #12345",
"return_url": "https://yourshop.com/checkout/complete",
"metadata": { "order_id": "12345" }
}'
const response = await fetch("https://api.tensorrail.com/payments", {
method: "POST",
headers: {
"api-key": process.env.TENSORRAIL_SECRET_KEY,
"Idempotency-Key": "8f14b6b0-2e2b-4c1a-9d5e-7a3c1f0b9e21",
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 50000,
currency: "INR",
description: "Order #12345",
return_url: "https://yourshop.com/checkout/complete",
metadata: { order_id: "12345" },
}),
});
const payment = await response.json();
import requests
response = requests.post(
"https://api.tensorrail.com/payments",
headers={
"api-key": "rail_full_test_xxx",
"Idempotency-Key": "8f14b6b0-2e2b-4c1a-9d5e-7a3c1f0b9e21",
},
json={
"amount": 50000,
"currency": "INR",
"description": "Order #12345",
"return_url": "https://yourshop.com/checkout/complete",
"metadata": {"order_id": "12345"},
},
)
payment = response.json()
The response carries the two values you redirect with:
{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "requires_payment_method",
"amount": 50000,
"currency": "INR",
"client_secret": "pay_N5cPeGw6uS2QIMjnsjVF_secret_k9Yt…",
"return_url": "https://yourshop.com/checkout/complete",
"metadata": { "order_id": "12345" }
}
Persist payment_id against your order before you redirect. Put your own order reference in
metadata; it is echoed on the payment object and every webhook, which is how you join the
outcome back to your order. Send an Idempotency-Key so a retried create can never produce two
payments. See Accept a payment.
Step 2: Redirect the customer
Build the redirect URL from the created payment, using your publishable key (pk), which
is browser-safe. Your secret key never leaves your server.
https://pay.tensorrail.com/?mode=link&payment_id=PAYMENT_ID&client_secret=CLIENT_SECRET&pk=rail_open_test_xxx&amount=50000¤cy=INR
| Query param | Value |
|---|---|
mode | link |
payment_id | from the created payment |
client_secret | from the created payment |
pk | your publishable key (rail_open_…) |
amount | display amount, minor units |
currency | display currency |
description | optional; the line the customer sees describing what they are paying for |
country | optional ISO 3166-1 alpha-2 code; disambiguates which market's methods to render when a currency is shared across markets |
setup_future_usage | optional; off_session on a save-method / subscription link |
merchant_return_url | optional; where to send the customer afterwards, when not taken from the payment |
The client_secret scopes the hosted page to exactly this one payment and nothing else, which
is what makes it safe to put a publishable key in the browser. A leaked publishable key cannot
create payments, list data, or refund anything. See Authentication.
- Node
- Python
// After creating the payment server-side, send the customer to the hosted page.
const url = new URL("https://pay.tensorrail.com/");
url.searchParams.set("mode", "link");
url.searchParams.set("payment_id", payment.payment_id);
url.searchParams.set("client_secret", payment.client_secret);
url.searchParams.set("pk", process.env.TENSORRAIL_PUBLISHABLE_KEY);
url.searchParams.set("amount", String(payment.amount));
url.searchParams.set("currency", payment.currency);
res.redirect(303, url.toString());
from urllib.parse import urlencode
query = urlencode({
"mode": "link",
"payment_id": payment["payment_id"],
"client_secret": payment["client_secret"],
"pk": TENSORRAIL_PUBLISHABLE_KEY,
"amount": payment["amount"],
"currency": payment["currency"],
})
redirect_url = f"https://pay.tensorrail.com/?{query}"
# e.g. Flask: return redirect(redirect_url, code=303)
Step 3: The customer pays
On the hosted page, TensorRail renders every method enabled for your account for that market and currency, and drives whatever the chosen rail requires: a redirect, an approval in a bank or wallet app, or reference details to pay to. You do not configure the method mix and you do not re-integrate to add one. When a new method or corridor is enabled for your account, it appears on the hosted page automatically, with no change on your side.
Step 4: Return and webhook
The customer is returned to your return_url when they finish. Returning is a UX signal,
not proof of payment. Local rails complete asynchronously, so the customer can arrive back
before the collection has cleared.
Treat a payment as paid only on the payment_succeeded webhook (or a retrieve returning
status: "succeeded"). Your return_url page should read the outcome, not assume it: look the
order up and show "payment received", "still processing", or "payment failed" based on the
current status, and let the webhook drive fulfilment.
// return_url handler: show state, do NOT fulfil here.
app.get("/checkout/complete", async (req, res) => {
const orderId = /* map from your session or a signed param */;
const order = await orders.get(orderId);
if (order.status === "paid") return res.render("thank-you");
if (order.status === "failed") return res.render("try-again");
return res.render("processing"); // webhook will update it
});
Fulfilment happens in your webhook handler. The webhook's content.object is the payment in
the same shape the REST API returns, so the fulfilment code you already have works unchanged.
See Handle webhooks.
Branding and return handling
- Display fields.
descriptionin the redirect URL populates the line the customer sees describing the purchase.countryselects which market's methods to render when a currency is shared across markets. The page reads no other display parameters — anything else you append is ignored rather than shown. - Return destination.
return_url(set on the payment, echoed in the object) is where the customer lands afterward. Use a URL you control, and key its content off the payment's current status rather than assuming success. - Statement descriptor. Set
statement_descriptor_nameon create to influence how the charge is described to the customer where the rail supports it.
Complete worked example
A minimal Node server: create the payment, redirect, handle the return, and fulfil on the webhook.
const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = process.env.TENSORRAIL_SECRET_KEY; // rail_full_…
const PK = process.env.TENSORRAIL_PUBLISHABLE_KEY; // rail_open_…
const WEBHOOK_SECRET = process.env.TENSORRAIL_WEBHOOK_SECRET;
// 1. Start checkout: create a payment and redirect to the hosted page.
app.post("/checkout", express.urlencoded({ extended: true }), async (req, res) => {
const orderId = req.body.order_id;
const r = await fetch("https://api.tensorrail.com/payments", {
method: "POST",
headers: {
"api-key": SECRET,
"Idempotency-Key": orderId, // one attempt per order
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 50000,
currency: "INR",
description: `Order ${orderId}`,
return_url: "https://yourshop.com/checkout/complete",
metadata: { order_id: orderId },
}),
});
const payment = await r.json();
await orders.setPaymentId(orderId, payment.payment_id);
const url = new URL("https://pay.tensorrail.com/");
url.searchParams.set("mode", "link");
url.searchParams.set("payment_id", payment.payment_id);
url.searchParams.set("client_secret", payment.client_secret);
url.searchParams.set("pk", PK);
url.searchParams.set("amount", String(payment.amount));
url.searchParams.set("currency", payment.currency);
res.redirect(303, url.toString());
});
// 2. Return page: reflect state only.
app.get("/checkout/complete", async (req, res) => {
res.send("Thanks. We'll confirm your order shortly.");
});
// 3. Webhook: verify, then fulfil once, idempotently.
app.post("/webhooks/tensorrail", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.get("TensorRail-Signature") || "";
const parts = Object.fromEntries(sig.split(",").map((kv) => kv.split("=")));
const expected = crypto
.createHmac("sha512", WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString()}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!fresh || expected !== parts.v1) return res.status(400).end();
const event = JSON.parse(req.body.toString());
if (event.content.object.status === "succeeded") {
fulfilOnce(event.content.object.metadata.order_id); // your idempotent fulfilment
}
res.status(200).end();
});
Next steps
Your hosted checkout is wired up. The natural next moves:
- Handle webhooks: harden the fulfilment handler that carries the real outcome.
- Payment links: reuse the same hosted page by sharing a link instead of redirecting.
- Testing and sandbox: validate the whole flow on test keys before going live.
TensorRail, Limassol, Cyprus.