Unirail

Route quotes

Priced routes between two accounts, labelled for your user to choose from, with a digest that binds their approval to exactly what they saw.

A route quote (qt_…) answers "how could this money move, what would it cost, and how long would it take?" for one payer account, one payee account and one amount.

const result = await unirail.routeQuotes.create({
  from: "acct_payer…",
  to: "acct_payee…",
  amount: { value: "5000", asset: "iso4217:GBP" },
  preference: "recommended", // optional; the environment's policy sets the default
});

Amounts are integer minor units in a string ("5000" is £50.00) plus an asset: iso4217:GBP for fiat, or a CAIP-19 id for on-chain assets. No float ever carries money.

What comes back

{
  quotes: RouteQuote[];        // up to three, labelled
  infeasible: InfeasibleRoute[]; // every route that can't work, and why
  suggestion?: "link-another-account"; // when nothing is feasible
}

Unirail checks every connected provider that could serve the pair against schemes, amounts, currencies, institution restrictions and your routing policy, then returns up to three quotes labelled cheapest, fastest and recommended. Show them to your user and let them choose.

FieldMeaning
labelcheapest, fastest or recommended.
legs[]The hops the money takes: from, to, rail, amount, custodyAfter and the userActions each needs.
totalCostpayer (what the payer pays in total) and a breakdown of fee lines, each with kind (provider, bank, platform, fx-spread, network) and paidBy (payer, payee or platform).
etap50Seconds and p90Seconds.
successLikelihoodBetween 0 and 1.
obligationsWhat you must do or show before this route can run, with the stage it applies to.
restrictionsAppliedRestrictions that shaped this route.
firmnessfirm, or indicative when the price can still move.
digestA hash of payer, payee, amount, legs and cost.
expiresAtAfter this, the quote can't be paid.

infeasible lists the routes that didn't make it, each with reasons and an explanation you can show. If nothing is feasible, Unirail suggests linking another account rather than blocking the user.

Binding your user's approval

The approval your user gives must cover exactly the route they saw. Bind it to the quote's id and digest, then pass both when you pay:

// 1. When the user picks a quote, derive the challenge they sign (a passkey, a PIN, a confirm step) from it.
const challenge = await sha256(`${quote.id}.${quote.digest}`);

// 2. After you've verified their approval, pay that quote.
const intent = await unirail.paymentIntents.create(
  { quote: quote.id, digest: quote.digest, returnUri },
  { context: { idempotencyKey: `payment:${payment.id}` } },
);

If the quote has expired, Unirail refuses with quote_expired. If anything the digest covers no longer holds, it refuses with quote_changed. In both cases quote again and ask the user again: never pay a route your user didn't approve.

Custody

Every leg says who holds the money after it (custodyAfter). Today's routes run bank to bank with custody none: the payer's bank pays the payee's bank directly and no one holds funds in between. Meta-providers such as Airwallex route through their own collection accounts, so their legs carry provider-transit. See payment intents for every value.

Routing policy

Each environment's routing policy, edited in the dashboard, decides:

  • which connections are eligible in which countries, and at which stage (off, internal, beta, ga, paused);
  • which custody kinds are admitted, so an environment that admits only none sees transit routes as infeasible, with that reason;
  • the default preference when a request doesn't set one.

Quote ranking is never paid placement.

On this page