Make your first routing decision with the Unirail API
Send masked metadata about a payment to the Unirail API and get back the provider to use, what it will cost under your contracts and why. Once we've set up your early-access account, you can create test keys in the dashboard.
unirail quickstart, test mode
The terminal runs in your browser and makes no API calls. Its responses use the real v1 response types
(RouteDecision, Outcome, RoutingSnapshot) with sample figures.
Quickstart
Five steps take you from an empty project to routing decisions that use your own outcomes. Each step works in test mode.
-
Get early access
Ask us for test access. Once we've set up your account, create an API key in the test environment. It starts with
ur_test_sk_. Copy the key when it's shown, because Unirail stores only a hash of it and its prefix.Decide mode also needs a fingerprint key of your own, 32 characters or more. Because the key never leaves your backend, Unirail can't reverse the account fingerprints you make with it.
Your server's environment export UNIRAIL_SECRET_KEY=ur_test_sk_… export UNIRAIL_FINGERPRINT_KEY=$(openssl rand -base64 32) -
Get the SDK
The SDK is TypeScript and runs on your server. It's the client for every v1 endpoint, plus webhook verification and fingerprints.
Placeholder The package isn't on npm yet. Access comes with your account. The command below will work once the package is published at launch.
Until then, every endpoint works over HTTPS with your test key. The next step makes your first decision with curl.
Install, once it's on npm pnpm add @unirail/sdk -
Ask for your first decision
Send the operation, the amount, the countries and schemes and a fingerprint of the payer's account. The response names the rail, its estimated cost and ETA, its success likelihood, ranked alternatives and every rail it ruled out, each with the reason.
Field names match the published v1 contract (RouteDecisionRequest, OutcomeRequest). Amounts are strings in minor units ("5000" is £50.00).
With curl, today curl https://api.unirail.dev/v1/route_decisions \ -H "Authorization: Bearer $UNIRAIL_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "unirail-version: 2026-10-08" \ -d '{ "operation": "banking.pis.single", "amount": { "value": "5000", "asset": "iso4217:GBP" }, "payer": { "country": "GB", "scheme": "scan" }, "payee": { "country": "GB", "scheme": "scan" }, "preference": "recommended" }'decide.ts, with the SDK import { createUnirail, maskAccount } from "@unirail/sdk"; const unirail = createUnirail({ apiKey: process.env.UNIRAIL_SECRET_KEY }); const decision = await unirail.routeDecisions.create({ operation: "banking.pis.single", amount: { value: "5000", asset: "iso4217:GBP" }, payer: { country: "GB", scheme: "scan", accountRef: await maskAccount("payto://scan/040004/12345678", process.env.UNIRAIL_FINGERPRINT_KEY), }, payee: { country: "GB", scheme: "scan" }, preference: "recommended", }); if (decision.status === "routed") { // Your backend calls decision.rail with your own credentials (@unirail/executor), // then reports what happened so the next decision learns from it. const started = Date.now(); const result = await callProvider(decision); // your code await unirail.outcomes.create({ decision: decision.id, rail: decision.rail, status: result.ok ? "succeeded" : "failed", latencyMs: Date.now() - started, }); } -
Report the outcome
Call the provider from your backend through
@unirail/executorand tell Unirail how it went. A retry doesn't count twice, because outcomes are idempotent on decision and status. Outcomes update reliability per rail, operation and bank, along with the contract usage the next decision is priced on.outcome.ts // After your backend has called the provider: await unirail.outcomes.create({ decision: decision.id, rail: decision.rail, status: "failed", latencyMs: 2300, failureCode: "rejected", }); -
Go live
Live keys start with
ur_live_sk_and belong to the live environment, with its own routing policy. Before you switch:- If Unirail will call providers for you, connect your Infisical vault. Unirail gets read-only access to one folder and keeps credentials in memory only, for up to five minutes. How credential federation works.
- Record your provider contracts as deals. Decisions are then priced under your real terms. Provider deal tracking.
- Get production access from each provider, under your own contract with it:
-
Plaid
Sandbox: self-serve · Production: under your contract
Production Payment Initiation needs Plaid's approval of the use case; VRP is enabled separately.
-
Yapily
Sandbox: self-serve · Production: under your contract
Under Yapily Connect, Yapily is the regulated party and the platform shows Yapily's disclosure. The catalogue lists a £15,000 cap per payment, not yet verified against a source.
-
Enable Banking
Sandbox: self-serve · Production: under your contract
Without a contract, production runs in restricted mode (own accounts only).
SDK packages and functions
Of the three packages, start with the SDK and add the executor when you move to Decide mode. The router is only for self-hosting, by arrangement.
@unirail/sdk
Not on npm yet
The API client, webhook verification and account fingerprints. Use it on your server only, because it takes a secret key.
@unirail/executor
Not on npm yet
The provider adapters Unirail uses, running inside your process with credentials from your own vault. Decide mode calls providers through it.
@unirail/router
The decision engine, for making decisions inside your own network from the routing snapshot. It's private and available by arrangement.
What @unirail/sdk exports
createUnirail({ apiKey })- A typed client for every v1 endpoint. Rejects a key that isn't ur_test_sk_… or ur_live_sk_….
webhooks.verify / sign / generateSecret- Check a delivery's Standard Webhooks signature against the raw body, or sign payloads in your tests.
maskAccount(payto, key)- Turn a payto:// address into an fp_ fingerprint with your HMAC key (32 characters or more).
canonicalPayto(payto)- Return an address in normalised form. The same account then always gives the same fingerprint.
paytoScheme(payto)- Return the scheme alone (scan or iban). Apart from the fingerprint, it's the only part of an address a Decide request carries.
keyMode(apiKey)- Return test or live, read from the key's prefix.
Webhooks
Webhook events are signed with the Standard Webhooks scheme. Verify the signature against the raw body before you parse it. If
a delivery goes missing, page through GET /v1/events with after to replay it.
import { webhooks } from "@unirail/sdk";
// Pass several secrets while you rotate them.
const event = await webhooks.verify({
body: await request.text(),
headers: request.headers,
secret: process.env.UNIRAIL_WEBHOOK_SECRET,
});
Next steps
- API referenceAll 23 v1 endpoints, including the 3 for Decide mode.
- Docs Concepts, guides and the full request and response schemas.
- Decide modeWhat a decision request carries and what stays in your backend.
- Self-hostingDecisions made inside your network against the routing snapshot, by arrangement.
- SecurityOIDC federation with your vault, hashed keys and signed webhooks.
- ChangelogWhat shipped in the v1 API and the SDK.