Self-host the routing engine in your own network
You can run @unirail/router, the decision engine behind POST /v1/route_decisions, inside your own infrastructure. It reads a routing snapshot from Unirail and decides each payment inside your network. Requests and decisions stay there. Unirail only receives the outcomes you report.
Self-hosting is by arrangement with us. Changes you make in the Unirail dashboard reach your router with the next snapshot, about a minute later.
What runs where
Unirail publishes the snapshot and takes outcomes back. Everything that touches a payment runs on your side.
app.unirail.dev
You edit routing policy, provider connections and deals here.
Outcome ingestion
Turns reported outcomes into success rates and this month's usage.
@unirail/router
Decides each payment from the snapshot. The request and the decision never leave your network.
@unirail/executor
Calls the chosen provider with keys from your own vault.
Plaid, Yapily or Enable Banking
You call it under your own contract.
GET /v1/routing_snapshot · POST /v1/outcomes
Run it
-
Get the package
@unirail/routerisn't on the public npm registry yet. You'll get access from us when we set up self-hosting with you. It runs in a Cloudflare Worker, Node or Bun process. -
Fetch the snapshot
Call
GET /v1/routing_snapshotwith your secret key, orunirail.routingSnapshot.get({})in the SDK. The key's environment decides which policy, connections and deals the snapshot holds. -
Make a decision
createRouter(snapshot).decide(request)takes the same masked request as the API and returns the same decision. -
Refresh the snapshot
Fetch again once
refreshAfterhas passed, 60 seconds aftergeneratedAt. Keep the old router until a fetch returns a newversion. Logdecision.snapshotso you know which version made each decision. -
Pay and report
Pay through
@unirail/executorwith your own credentials. Then report toPOST /v1/outcomeswith acontextof operation and country, because Unirail never saw the decision.
import { createUnirail } from "@unirail/sdk";
import { createRouter } from "@unirail/router";
const unirail = createUnirail({ apiKey: process.env.UNIRAIL_SECRET_KEY });
let snapshot = await unirail.routingSnapshot.get({});
let router = createRouter(snapshot);
// Refetch once refreshAfter passes. Keep the old router until the version changes.
async function currentRouter() {
if (Date.now() >= Date.parse(snapshot.refreshAfter)) {
const next = await unirail.routingSnapshot.get({});
router = next.version === snapshot.version ? router : createRouter(next);
snapshot = next;
}
return router;
}
const decision = await (await currentRouter()).decide({
operation: "banking.pis.single",
amount: { asset: "iso4217:GBP", band: { min: "0", max: "10000" } },
payer: { country: "GB", scheme: "scan" },
payee: { country: "GB", scheme: "scan" },
});
if (decision.status === "routed") {
// Pay through decision.rail with @unirail/executor and your own credentials, then report.
// Unirail never saw this decision, so the outcome carries its context.
await unirail.outcomes.create({
decision: decision.id,
rail: decision.rail,
status: "succeeded",
latencyMs: 1840,
context: { operation: "banking.pis.single", country: "GB" },
});
}
What's in the snapshot
The snapshot is everything the engine reads for one environment:
- the environment and whether it's test or live;
- your routing policy, with its version, the default preference and which providers may run what in each country and in what order;
- the ranking weights for cost, success and link reuse, plus FX rates;
- each enabled connection's provider, with the roles its adapter declares and the catalogue's capabilities by country;
- active deals with this month's usage, published prices and success rates per provider, operation and bank.
None of it is a credential, a secret or data about a person. Its version is a hash of the content and
changes only when something the engine reads changes.
The excerpt shows one entry from each list in the example snapshot behind this site's routing examples. Its contracts, usage and success rates are made up.
{
"object": "routing_snapshot",
"version": "snap_9e6621887557d7ab340533fef55cc4fe",
"generatedAt": "2026-10-18T09:00:00.000Z",
"refreshAfter": "2026-10-18T09:01:00.000Z",
"environment": {
"id": "env_example",
"mode": "live"
},
"policy": {
"version": 7,
"defaultPreference": "recommended",
"document": {
"bindings": [
{
"gates": [],
"order": 0,
"ref": {
"jurisdiction": "GB",
"providerId": "plaid",
"role": "payment"
},
"stage": "ga"
}
],
"custodyAdmissions": [],
"environment": "production"
}
},
"selection": {
"fxFeeBps": 300,
"fxRates": {},
"weights": {
"cost": 1,
"reuse": 1,
"success": 1
}
},
"rails": [
{
"capabilities": [
{
"countries": [
"GB"
],
"id": "banking.pis.single",
"status": "integrated"
},
{
"countries": [
"AT",
"BE",
"DE",
"DK",
"EE",
"ES",
"FI",
"FR",
"IE",
"IS",
"IT",
"LT",
"LV",
"NL",
"NO",
"PT",
"SE"
],
"id": "banking.pis.single",
"status": "listed",
"source": "https://docs.yapily.com/payments/payment-resources/european-payments",
"checkedAt": "2026-10-08"
}
],
"connection": "con_exYapily1",
"providerEnvironment": "production",
"rail": "yapily",
"roles": [
{
"coverage": [
{
"country": "GB",
"currency": "GBP"
}
],
"custody": "none",
"imposedObligations": [
"O-DISCLOSE",
"O-REVOKE-SURFACE"
],
"payeeSchemes": [
"scan"
],
"payerSchemes": [
"scan"
],
"role": "payment",
"schemeTimings": [
{
"scheme": "scan",
"statusDeadlineSeconds": 7200
}
],
"userActions": [
"redirect"
],
"maxAmount": {
"asset": "iso4217:GBP",
"value": "1500000"
}
}
],
"status": "ready"
}
],
"contracts": [
{
"currency": "GBP",
"deal": "dl_example_yapily",
"effectiveFrom": "2026-06-01T00:00:00.000Z",
"fixed": [],
"periodStart": "2026-10-01T00:00:00.000Z",
"rail": "yapily",
"units": [
{
"blocks": [
{
"size": 15000,
"unitPrice": 0.15
}
],
"overageUnitPrice": 0.2,
"unit": "payment"
},
{
"blocks": [
{
"size": 25000,
"unitPrice": 0.25
}
],
"overageUnitPrice": 0.2,
"unit": "link"
}
],
"usage": [
{
"unit": "payment",
"used": 8960
},
{
"unit": "link",
"used": 1020
}
],
"version": 1
}
],
"listPrices": [],
"reliability": [
{
"attempts": 1262,
"operation": "banking.pis.single",
"p50Seconds": 11,
"p90Seconds": 74,
"rail": "yapily",
"successes": 1205
}
]
}
What still needs Unirail
The snapshot still comes from Unirail, because you edit policy, deals and connections in the control plane. Unirail also turns the outcomes you report into the success rates and contract usage that every router reads from the snapshot. See how deals price each decision and how the routing engine ranks providers.
Common questions
Is the self-hosted engine the same one the API runs?
Unirail's API runs @unirail/router behind POST /v1/route_decisions. Self-hosting runs the same package against the same snapshot. Decisions come back in the same shape, with the same dec_ ids and digests.
How fresh is the snapshot?
Each snapshot tells your router to fetch the next one 60 seconds after it was generated. Changes to policy and deals reach your router within that window. Success rates move slowly enough that a minute makes no difference.
Does the snapshot contain secrets or personal data?
The snapshot holds your routing policy, the providers' declared roles and catalogue capabilities, contract terms with this month's usage, published prices and success rates. None of it is a credential or data about a person.
What does Unirail see when we self-host?
Unirail sees only the outcomes you report. Each one carries the decision id, the provider used, the status and latency, the operation and country as context and, if you send them, the fee and a short failure code. Requests and decisions stay with you.
What happens if we stop reporting outcomes?
The router will keep deciding on the last success rates it saw. Tier prices will also stay where they were, because contract usage stops moving.
Is there a container image?
There's no container image yet. The router is a TypeScript package whose only platform dependency is Web Crypto. It runs in a Cloudflare Worker, Node or Bun process.
Run the engine next to your payments
Tell us where the router would run and which providers it would choose between. We'll give you the package and a test environment to fetch snapshots from.