Skip to main content

Direct API

The direct API is the integration path where you render your own checkout UI and drive the payment yourself: create the payment, present the enabled methods, confirm the customer's choice, handle whatever next_action the rail requires, and confirm the outcome by webhook. It gives you the most control over the payment experience, and in return you own the most responsibility.

An integration path, not a separate product

"Direct API" is one of three ways to integrate onto the one TensorRail platform, alongside hosted checkout and payment links. It is the same account, the same balance, the same webhooks, the same error catalog, and the same payment object as the other paths. The only difference is that here you draw the payment screen instead of TensorRail. It is not a different product, a different host, or a different pricing model.

When to use it​

  • The checkout is part of your product's core UX and you want to control every pixel.
  • You need to embed payment collection inside an existing flow (an app, a portal, a POS).
  • You are prepared to handle next_action, decline UX, and dynamic method rendering yourself.

If you want the fastest launch with no UI to maintain, use hosted checkout. You can start hosted and move to the direct API later without changing your create call, webhooks, or reconciliation, because those are identical across paths.

What you own on this path​

The hosted page carries three things for you that you take on with the direct API:

  • next_action handling. Forward the customer to whatever the confirm response asks for (a redirect, an in-app approval, reference details), treating its shape as opaque.
  • Decline UX. A failed payment is terminal for that payment; offer the customer a fresh attempt (a new payment, a new Idempotency-Key).
  • Dynamic method rendering. Present the methods enabled for your account rather than hard-coding them, so a newly enabled method appears without a release.

The flow​

  1. Create the payment server-side with your secret key.
  2. Present the enabled methods in your own UI.
  3. Confirm the customer's chosen method.
  4. Handle next_action if the rail needs a customer action.
  5. Confirm the outcome by webhook.

Discover the methods enabled for your account​

GET /account/payment_methods: authenticate with your publishable key, the same one the browser holds. It answers with the payment methods and method types enabled on your account, in the environment your key belongs to, which is what "present the enabled methods" above means in practice. Read it when you render the checkout rather than hard-coding a list, and a method enabled for you later appears without a release on your side.

curl "https://api.tensorrail.com/account/payment_methods" \
-H "Authorization: Bearer rail_open_live_xxx"

The response groups method types under each payment method, with the currencies and any experience hints for each. Two fields are always empty on this surface; do not branch on them. Routing is decided server-side.

Do not confuse it with GET /v1/capabilities, which is the unauthenticated, platform-wide catalogue of what TensorRail supports: it is not a per-account list and it will show you methods your account does not have.

Step 1: Create​

POST /payments. amount and currency are required. Full create fields, idempotency, and the response are documented in Accept a payment; the short version:

curl -X POST https://api.tensorrail.com/payments \
-H "api-key: rail_full_test_xxx" \
-H "Idempotency-Key: 3d9a1c22-4e77-4b0a-8f21-2c6d9b0e5a44" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "INR",
"return_url": "https://yourshop.com/checkout/complete",
"metadata": { "order_id": "12345" }
}'

Persist payment_id, and pass client_secret to the browser only if you confirm client-side (below).

Step 2: Confirm​

POST /payments/{payment_id}/confirm with the customer's chosen method. Send payment_method_type explicitly; omitting it can block a later refund. Full confirm fields are 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" },
"return_url": "https://yourshop.com/checkout/complete"
}'

You can also confirm from the browser with your publishable key plus the payment's client_secret, keeping the secret key server-side, from any of your own pages whose origin is enabled for your account. See Authentication.

Step 3: Handle next_action​

Most local rails need the customer to do something after confirm. The confirm response comes back with status: "requires_customer_action" and a next_action object:

{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "requires_customer_action",
"amount": 50000,
"currency": "INR",
"payment_method": "upi",
"payment_method_type": "upi_intent",
"next_action": { "…": "…" },
"return_url": "https://yourshop.com/checkout/complete"
}

next_action describes what the customer must do. Its inner shape depends on the action, so treat it as opaque: hand the customer to it without branching on the method. The three shapes you will encounter, and what to do with each:

Action kindWhat the customer doesWhat you do
RedirectCompletes on a page the rail hosts (for example a 3DS or bank page)Send the customer to the provided URL; they return to your return_url
In-app approvalApproves a prompt in a bank or wallet appShow a "we sent you a prompt, approve it, we'll update this page" state and wait for the webhook
Reference to payPays to displayed account / reference details (for example a virtual account or voucher)Display the details for the customer to pay, then wait for the webhook

The safe pattern that works across all three without special-casing:

function handleNextAction(payment, res) {
switch (payment.status) {
case "requires_customer_action": {
// Forward to a redirect if the action provides a URL; otherwise render a
// wait screen. Treat next_action as opaque: read a URL if present, else display.
const url = extractRedirectUrl(payment.next_action); // may be undefined
if (url) return res.redirect(303, url);
return res.render("await-approval", { next_action: payment.next_action });
}
case "processing":
return res.render("processing"); // rail is settling; wait for the webhook
case "succeeded":
return res.render("thank-you");
case "failed":
return res.render("try-again", { error_code: payment.error_code });
default:
return res.render("processing");
}
}

After the customer acts, the payment moves to processing and resolves via webhook. Do not poll in a tight loop. Wait for the webhook, and keep GET /payments/{payment_id} for reconciliation and recovery.

Step 4: Confirm the outcome by webhook​

The customer returning to return_url is a UX signal, not proof of payment. Treat a payment as paid only on the payment_succeeded webhook (or a retrieve returning succeeded). The webhook content.object is this same payment object. See Handle webhooks.

Handling declines​

A declined collection is not an HTTP error. The call succeeds and the payment object reports status: "failed" with a stable error_code:

{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"status": "failed",
"error_code": "ERR_4094",
"error_message": "The payment was declined."
}

failed is terminal for that payment. When the customer wants to try again, create a new payment with a new Idempotency-Key; do not re-confirm the failed one. Look the error_code up in the Error catalog to see whether a new attempt can succeed; Error intelligence has the handling recipe.

Complete worked example​

Server-side create then confirm, driving the customer through next_action, with fulfilment on the webhook. (Webhook verification is shown in full in Handle webhooks.)

const express = require("express");
const app = express();
const SECRET = process.env.TENSORRAIL_SECRET_KEY;

async function tr(path, body) {
const r = await fetch(`https://api.tensorrail.com${path}`, {
method: "POST",
headers: { "api-key": SECRET, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return r.json();
}

// Customer submits your checkout form.
app.post("/pay", express.json(), async (req, res) => {
const { order_id, method, method_type, method_data, email } = req.body;

const payment = await tr("/payments", {
amount: 50000,
currency: "INR",
return_url: "https://yourshop.com/checkout/complete",
metadata: { order_id },
});
await orders.setPaymentId(order_id, payment.payment_id);

const confirmed = await tr(`/payments/${payment.payment_id}/confirm`, {
payment_method: method,
payment_method_type: method_type, // always send this
payment_method_data: method_data,
customer: { email },
return_url: "https://yourshop.com/checkout/complete",
});

return handleNextAction(confirmed, res); // from Step 3
});

// Webhook: verify (see Handle webhooks), then fulfil once.
app.post("/webhooks/tensorrail", express.raw({ type: "*/*" }), (req, res) => {
if (!verifyTensorRailWebhook(req.body, req.get("TensorRail-Signature"), process.env.TENSORRAIL_WEBHOOK_SECRET)) {
return res.status(400).end();
}
const event = JSON.parse(req.body.toString());
const payment = event.content.object;
if (payment.status === "succeeded") fulfilOnce(payment.metadata.order_id);
if (payment.status === "failed") markFailed(payment.metadata.order_id);
res.status(200).end();
});

Next steps​

Your own checkout can create, confirm, and drive next_action. Go deeper:

  • Accept a payment: the full create / confirm / decline / capture reference for this path, with every field.
  • Capture and cancel: switch to authorize-then-capture when you ship before you charge.
  • Handle webhooks: the authoritative outcome signal behind your fulfilment.

TensorRail, Limassol, Cyprus.