unirail

01 A payment in nine scenes

One API for everypayment rail.

Unirail is routing intelligence for apps that move money: one API over the PISPs, aggregators and KYC vendors you already have deals with, a priced and reasoned decision for every payment, and your credentials left in your own vault. Funds go bank to bank, never through Unirail.

Scroll to follow one payment

ONE APIFRAME 01
Frame 01 · Every provider's rail converges into one API

02 The ask

Two people, two banks, one tap.

Maya owes Theo £50. Your app holds both accounts as payto:// addresses and sends Unirail masked metadata only, no names and no account numbers, with one question: which way should this go?

Pay Theo
Concert tickets
£50.00
Frompayto://scan/••••••/••••4821
Topayto://scan/••••••/••••0193
Pay £50.00Finding routes…
PAYER'S BANK · GBPAYEE'S BANK · GBUNIRAIL · DECISION ENGINEMASKED REQUEST · NO PII£50FRAME 02
Frame 02 · Two banks; a masked request goes up to the decision engine above the network

03 Route quotes

Every way there, priced before anyone picks.

The decision comes back as route quotes: legs, cost, ETA and custody for each feasible path, priced from your own provider contracts, with the reason each one exists. Routes your policy doesn't admit never reach the list.

Illustrative quote · values are examples

Cheapest

1 leg
Cost
£0.10
ETA p50
2 min
Likely
97%
Custody
none

Fastest

1 leg
Cost
£0.25
ETA p50
6 s
Likely
95%
Custody
none

Via a meta-provider

2 legs
Custody
provider-transit
Policy
not admitted
DECISIONRECOMMENDEDFASTESTCHEAPESTCUSTODY ROUTE · FILTERED BY POLICYFRAME 03
Frame 03 · The decision comes down: three quoted routes, a custody route filtered out by policy

04 Approval

The yes is sealed to the quote.

Your platform binds its user's approval, a passkey for instance, to the quote's id anddigest. If payer, payee, amount, legs or cost change afterwards, the intent fails withquote_changed instead of moving different money.

Maya approves with a passkey
challenge = quote id + digest
Quoteqt_7Hq2Lm9Xa4
Digest3f9c0d7a…b48e21a
Signal cleared
Approval bound · quote locked
QUOTEqt_7Hq2Lm9Xa4DIGEST3f9c…e21aAPPROVALPASSKEY · BOUNDFRAME 04
Frame 04 · The approval is bound to the quote's id and digest; the signal clears

05 In transit

Bank to bank. Nothing in between.

Your backend calls the provider Unirail picked, with your own credentials, through the SDK. The money goes from Maya's bank straight to Theo's and never passes through Unirail; custody stays none.

pi_4Rk8Ta2Wq9 · leg_1 · £50.00
  1. createdquote accepted
  2. awaiting-userMaya approves in her bank app
  3. submittedyour PISP initiates it
  4. acceptedpayer's bank accepts
  5. creditedTheo's bank credits £50
custodyAfternone
✓ NAME CHECK✓ BANK APPROVAL→ SUBMITTED CREDITEDCUSTODY · NONEBANK TO BANK · NEVER THROUGH UNIRAILFRAME 05
Frame 05 · The payment travels bank to bank, under the engine, never through it; custody none

06 Arrival

It lands. You hear about it once.

Whichever provider ran it, the outcome reaches your app as one event type, signed per Standard Webhooks. Verify it once and the same code holds for every bank, PISP and country behind it.

POST /webhooks/unirailwebhook-id: evt_9Jd2Kp4Lx7webhook-timestamp: 1791489602webhook-signature: v1,Zm9vYmFy…Q2s= { "object": "event", "type": "payment_intent.succeeded", "mode": "test", "data": { "object": { "id": "pi_4Rk8Ta2Wq9" } } }
YOUR APPpayment_intent.succeededCREDITED · PAYEE'S BANKFRAME 06
Frame 06 · The payee's bank is credited and one signed event returns to your app

07 Two modes

Decide, or execute. Your keys stay home.

Your licences, your provider deals, your KYC. Unirail plugs into the secrets manager you already run (Infisical first), so adding a provider never means new env vars.

  1. DecideMasked metadata in, a decision out. Your backend calls the provider with its own keys; Unirail never touches secrets or money.
  2. Execute via UnirailUnirail makes the call, reading the credential just in time from your vault over OIDC.
  3. Self-host the engineRun the decision engine inside your own infrastructure.
Provider credentials stored by Unirail0
YOUR BACKENDEXECUTE · JUST-IN-TIME OVER OIDCDECIDE · YOUR OWN READYOUR INFISICALUNIRAIL · STORED: 0FRAME 07
Frame 07 · Decide mode: your backend reads its own key. Execute mode: Unirail reads it just in time. Stored: nothing

08 Deals and the marketplace

Your deals, next to the routes.

Contracts, fees and volume tiers sit beside live routing data, so every negotiation starts from what switching would save. The marketplace shows who else carries each capability, country by country: banking first, then KYC, account checks and compliance.

Deal · PISP AIllustrative
Contractper-payment fee, 3 volume tiers
This month14,200 of 20,000 to tier 2
Switch to Bsaves £1,240 / mo at this mix
3Integrated
10Listed
23Countries
  1. 01Bankingbanking.pis.single · banking.ais.* · banking.payoutslive
  2. 02Identity · KYCidentity.kyc.document · identity.kybGB
  3. 03Account verificationverification.cop · verification.voplisted
01 BANKINGais · pis · payouts02 IDENTITYkyc · kyb03 VERIFICATIONcop · vop04 COMPLIANCEscreening · nextFRAME 08
Frame 08 · Capabilities layered over 23 countries, with your deals beside the routes

09 Your turn

A quote and an intent. That's the integration.

The SDK takes your secret key and the two payto:// accounts, and your user's approval binds to what comes back. Everything around it lives in the dashboard.

  • Test and live environments, each with its own API keys
  • Routing policy: preferences and which custody you admit
  • Deals, fees and volume tiers beside live routing data
  • Decide mode, Execute mode, or a self-hosted decision engine
pay.ts@unirail/sdk
import { createUnirail } from "@unirail/sdk";

const unirail = createUnirail({ apiKey: process.env.UNIRAIL_SECRET_KEY });

// Every feasible route, priced: cheapest, fastest, recommended.
const { quotes } = await unirail.routeQuotes.create({
  from: payer, to: payee,
  amount: { value: "5000", asset: "iso4217:GBP" },
});

// Bind your user's approval to id + digest, then run it.
const intent = await unirail.paymentIntents.create(
  { quote: quotes[0].id, digest: quotes[0].digest, returnUri },
  { context: { idempotencyKey: `pay:${id}` } },
);
createUnirail({ apiKey })FRAME 09
Frame 09 · The mark: many rails merging into one line, and the payment arriving