Route quotes
Ask for a route between two accounts and get up to three quotes back, each with its legs, total cost, ETA and custody spelled out.
unirail.dev the GDS for payments
Unirail sits between apps that move money and the providers that can move it. Ask for a route, get back the cheapest, fastest and recommended paths, and bind your user's approval to the exact one they pick.
$ pnpm add @unirail/sdk
# server-side only: ur_test_sk_… or ur_live_sk_…
$ export UNIRAIL_SECRET_KEY=ur_test_sk_…const { quotes } = await unirail.routeQuotes.create({
from: payer, to: payee,
amount: { value: "5000", asset: "iso4217:GBP" },
});const intent = await unirail.paymentIntents.create(
{ quote: quotes[0].id, digest: quotes[0].digest, returnUri },
{ context: { idempotencyKey: `pay:${id}` } },
);01 what it does
Unirail doesn't hold your money or your keys. It knows which providers can move a payment between two accounts, what each path costs and how long it takes, and it hands your app one shape for all of them.
Ask for a route between two accounts and get up to three quotes back, each with its legs, total cost, ETA and custody spelled out.
Every account is a payto:// address. Unirail returns it masked with a keyed fingerprint, never in full.
Your user's passkey signs a quote's id and digest. If anything moves afterwards, the intent fails with quote_changed.
Provider credentials live in your own Infisical. Unirail reads them per call through OIDC federation and never stores them.
Every provider's statuses arrive as one set of events, delivered and signed per Standard Webhooks.
Environments, API keys and routing policy live in the dashboard. Every row is scoped to an environment, and so is every key.
ur_test_sk_••••••••••••••••••••••••3 providers integrated, 10 listed, and the same shape for what comes next: identity, account verification, compliance.
02 route quotes
Ask for a route between two accounts. Unirail returns up to three quotes, each with its legs, total cost, ETA, success likelihood and who holds the money after every leg. Routes your policy forbids come back as infeasible, with the reason, so nothing is silently dropped.
unirail.routeQuotes.createsample response · illustrative valuescustody: noneDemo: binds the selected quote's id and digest.
03 credentials
You keep your contracts with Plaid, Yapily or Enable Banking, and their keys stay in your own Infisical. Unirail is trusted through OIDC federation and reads a key only for the call that needs it.
Unirail signs a short-lived OIDC token as its own issuer, scoped to your environment.
Your Infisical trusts that issuer and returns the provider credential for this one call.
Unirail calls the provider and lets the credential go. It never reaches a database, a log, a trace or an error.
04 marketplace
Banking comes first, then everything next to it: identity, account verification, compliance. Each capability is a rail with several providers on it, per jurisdiction, so a route can switch providers without your code noticing.
05 sdk
Quote, then pay with the quote your user approved. Every write takes an idempotency key you derive from your own record, and every status change arrives on one signed event stream.
ur_test_sk_… and ur_live_sk_… keys pick the environmentimport { createUnirail } from "@unirail/sdk";
const unirail = createUnirail({ apiKey: process.env.UNIRAIL_SECRET_KEY });
// Up to three quotes: cheapest, fastest, recommended.
const { quotes } = await unirail.routeQuotes.create({
from: payer,
to: payee,
amount: { value: "5000", asset: "iso4217:GBP" },
});
// Your user approved quotes[0] with a passkey bound to its id + digest.
const intent = await unirail.paymentIntents.create(
{ quote: quotes[0].id, digest: quotes[0].digest, returnUri },
{ context: { idempotencyKey: `pay:${id}` } },
);import { webhooks } from "@unirail/sdk";
export default {
async fetch(request: Request, env: Env) {
// Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature.
const event = await webhooks.verify({
body: await request.text(),
headers: request.headers,
secret: env.UNIRAIL_WEBHOOK_SECRET, // or [old, new] while rotating
});
if (event.type === "payment_intent.succeeded") {
await markPaid(event.data.object.id);
}
return new Response(null, { status: 204 });
},
};06 control plane
Decide which providers a route may use, whether money may sit with a meta-provider in transit, and which environment a key can touch, then change it without a deploy.
Control-plane screenshot goes here
Swap in a capture of app.unirail.dev at 1600 × 1000 once the dashboard design settles. The sidebar lists what it will show.
07 get started
ur_test_sk_ key