What runs where

Unirail publishes the snapshot and takes outcomes back. Everything that touches a payment runs on your side.

Unirail, control plane

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.

Your network

@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.

Provider

Plaid, Yapily or Enable Banking

You call it under your own contract.

Self-hosted Decide mode · what runs where GET /v1/routing_snapshot · POST /v1/outcomes

Run it

  1. Get the package

    @unirail/router isn'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.

  2. Fetch the snapshot

    Call GET /v1/routing_snapshot with your secret key, or unirail.routingSnapshot.get({}) in the SDK. The key's environment decides which policy, connections and deals the snapshot holds.

  3. Make a decision

    createRouter(snapshot).decide(request) takes the same masked request as the API and returns the same decision.

  4. Refresh the snapshot

    Fetch again once refreshAfter has passed, 60 seconds after generatedAt. Keep the old router until a fetch returns a new version. Log decision.snapshot so you know which version made each decision.

  5. Pay and report

    Pay through @unirail/executor with your own credentials. Then report to POST /v1/outcomes with a context of operation and country, because Unirail never saw the decision.

Self-hosted decide and report
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.

Example snapshot, one entry per list
{
  "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.