Skip to main content

Quickstart: your first 10 minutes

This guide takes you from zero to a payment and a webhook. It uses UPI (India) as the concrete example, but the flow is the same for every method: you integrate once, and the methods available to a customer are the ones enabled for your account. You never hard-code a rail.

Prefer to click around first? The full API is downloadable as an OpenAPI spec at /openapi/tensorrail-merchant.json and imports straight into Postman or Insomnia.

You need: a TensorRail account with dashboard access, your test keys, and (for step 4) an HTTPS URL that reaches your machine (a tunnel to localhost is fine during development).

1. Get your keys

From the dashboard, go to Developers → API Keys and copy your test keys:

  • Secret key rail_full_test_… for server-to-server calls.
  • Publishable key rail_open_test_… for anything in the browser.

Build against test keys first. The key selects the environment; there is one base URL, https://api.tensorrail.com. See Authentication.

2. Create a payment

Send an amount (smallest currency unit) and a currency. Include an Idempotency-Key so a network retry cannot create a duplicate: retrying with the same key replays the original request and returns the original payment, whatever the retry's body says. Never reuse a key with a different body — there is no mismatch rejection; the differing body is silently ignored. (The header is honored only when the body does not supply its own payment_id.)

curl -X POST https://api.tensorrail.com/payments \
-H "api-key: rail_full_test_xxx" \
-H "Idempotency-Key: 5f0c9e2a-1b7d-4d3e-9a1c-6b2f8e4a1c33" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "INR",
"customer": { "email": "buyer@example.com" },
"description": "Order #12345",
"metadata": { "order_id": "12345" },
"return_url": "https://yourshop.com/checkout/complete"
}'

Amounts are in the smallest unit (paise for INR): 50000 = INR 500.00. You get back the payment object; the two fields you must keep are payment_id (store it against your order) and client_secret (hand it to the browser):

{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "requires_payment_method",
"amount": 50000,
"currency": "INR",
"client_secret": "pay_N5cPeGw6uS2QIMjnsjVF_secret_…",
"description": "Order #12345",
"metadata": { "order_id": "12345" },
"return_url": "https://yourshop.com/checkout/complete"
}

If this call fails, the body is an error object; the usual first-run causes:

You seeCause
ERR_1002Missing or wrong api-key header value
ERR_2001A required field (for example amount) missing; the field is named in message
ERR_2002currency is not a well-formed ISO 4217 code. A valid code you have no corridor for is accepted here and fails later at confirm with ERR_4088 — see Corridors and coverage
ERR_3051Test/live mode mismatch: check which key you loaded

3. Present and confirm

You have two choices, and this is the only decision that matters. See Choose your integration.

Fastest: the hosted page. Redirect the customer to pay.tensorrail.com with the payment's identifiers and your publishable key; TensorRail renders every enabled method and returns the customer to your return_url:

https://pay.tensorrail.com/?mode=link&payment_id=pay_N5c…&client_secret=pay_N5c…_secret_…&pk=rail_open_test_xxx&amount=50000&currency=INR

Full control: confirm yourself. Render the enabled methods and confirm the customer's choice from your own checkout. The full field reference is in Accept a payment:

curl -X POST https://api.tensorrail.com/payments/pay_N5c…/confirm \
-H "api-key: rail_full_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"payment_method": "upi",
"payment_method_type": "upi_intent",
"payment_method_data": { "upi": { "upi_intent": {} } },
"customer": { "email": "buyer@example.com" },
"billing": { "email": "buyer@example.com" }
}'

The response comes back with status: "requires_customer_action" and a generic next_action: for UPI, where to send the customer so they can approve in their bank app. Forward the customer to next_action generically, without inspecting what kind of action it is; that keeps your checkout working as new methods are added. Always send payment_method_type explicitly; omitting it can block later refunds.

4. Receive a webhook

The customer returning to your return_url is not proof of payment; local rails complete asynchronously. Treat a payment as paid only on the payment_succeeded webhook (or a retrieve returning status: "succeeded").

Set your webhook URL and copy your signing secret in the dashboard under Developers → Webhooks, then run a minimal handler:

const express = require("express");
const crypto = require("crypto");

const app = express();
const SECRET = process.env.TENSORRAIL_WEBHOOK_SECRET;

app.post("/webhooks/tensorrail", express.raw({ type: "*/*" }), (req, res) => {
const header = req.get("TensorRail-Signature") || "";
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const expected = crypto
.createHmac("sha512", SECRET)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!fresh || expected.length !== (parts.v1 || "").length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
return res.status(400).end();
}

const event = JSON.parse(req.body);
if (event.content.object.status === "succeeded") {
// fulfil the order for event.content.object.metadata.order_id, idempotently
}
res.status(200).end();
});

app.listen(4242);

The event you are waiting for:

{
"event_id": "evt_9f2c…",
"event_type": "payment_succeeded",
"timestamp": "2026-07-22T09:03:11Z",
"content": {
"type": "payment_details",
"object": {
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "succeeded",
"amount": 50000,
"currency": "INR",
"metadata": { "order_id": "12345" }
}
}
}

The full contract (event types, de-duplication, retries) is in Handle webhooks. As a fallback or for reconciliation, you can always retrieve the current state directly:

curl https://api.tensorrail.com/payments/pay_N5c… \
-H "api-key: rail_full_test_xxx"

The collected value is now on your balance. Read it with GET /v1/balances — see Balances.

What you just built works for every method

You wrote a create call, a way to present options, and a webhook handler. That same code takes mobile money, bank transfer, and every other rail enabled for you, with no changes. It is also already production-shaped: idempotent creates, signature-verified webhooks, and fulfilment keyed on status, which is exactly what Go live checks for.

Next steps

You have a payment and a verified webhook. From here:

  • Choose your integration: decide between hosted checkout, payment links, and the direct API now that you have seen the create call.
  • Accept a payment: the create and confirm fields in full, plus declines, retries, and manual capture.
  • Handle webhooks: the full event contract, ordering, and signature verification behind the handler you just wrote.

TensorRail, Limassol, Cyprus.