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.

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. 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. Before you ever see failed, the platform may already have re-attempted a recoverable decline on an alternative route inside the same payment. See Error intelligence and Orchestration and routing.

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.