Sample content

Engineering

How a payment finds the fastest open rail

One POST, five rails. Inside the router that picks RTP, same-day ACH or a wire — and what it does when a rail says no.

On this page
  1. The decision, in one diagram
  2. When a rail says no
  3. Webhooks close the loop
  4. Notes

Most payment APIs make you choose the rail. You pick ACH or wire, hard-code it, and find out at 4:59 p.m. on a Friday that the choice was wrong. We wanted the opposite: describe the payment, and let the router pick the fastest rail that can legally and reliably carry it.

This post walks through that router — the inputs it reads, the order it evaluates them, and the part nobody diagrams: what happens when the rail it picked says no.

The decision, in one diagram

Every payment carries three facts the router cares about: amount, the receiving institution’s capabilities, and the clock. Everything else is policy you can override per call.

Routing decision: if the amount is within the RTP limit and the receiver is on RTP, the payment goes by RTP. If the amount is over the limit it goes by wire; if the receiver is not on RTP it goes by ACH, same-day if before cutoff. PAYMENT amount ≤ RTP limit? yes receiver on RTP? yes RTP no WIRE no ACH same-day if before cutoff
Fig. 2 — The routing decision, simplified. A policy override such as rail: "ach" skips the tree entirely.

In code, you never name the rail. You state the constraint — here, “arrive within minutes” — and read back which rail carried it.

import { Omniwire } from "@omniwire/sdk";
const omniwire = new Omniwire(process.env.OMNIWIRE_KEY);

const payment = await omniwire.payments.create({
  amount: 1840_00,              // $1,840.00, in cents
  counterparty: "cp_8f2k…",
  arrive: "minutes",            // the constraint, not the rail
  idempotencyKey: "inv_2931",
});

payment.rail;    // "rtp"
payment.status;  // "settled"
import os
from omniwire import Omniwire
omniwire = Omniwire(os.environ["OMNIWIRE_KEY"])

payment = omniwire.payments.create(
    amount=1840_00,              # $1,840.00, in cents
    counterparty="cp_8f2k…",
    arrive="minutes",            # the constraint, not the rail
    idempotency_key="inv_2931",
)

payment.rail     # "rtp"
payment.status   # "settled"
curl -X POST "$OMNIWIRE_API_BASE/payments" \
  -H "Authorization: Bearer $OMNIWIRE_KEY" \
  -H "Idempotency-Key: inv_2931" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 184000, "counterparty": "cp_8f2k…", "arrive": "minutes" }'

# → { "rail": "rtp", "status": "settled", … }

Package: @omniwire/sdk

When a rail says no

An RTP send can be rejected in milliseconds — the receiving bank is offline, the account can’t accept credits, or the amount trips a participant limit.1 Because RTP is final, a rejection is the good outcome: no money moved, and we can try the next rail without risking a duplicate.

Webhooks close the loop

Whichever rail carries the money, you receive the same event shape: payment.settled with the rail, the network reference and the final amount.2 Your ledger code never branches on rail.

Footnotes

  1. Participant limits are set by the network and by each receiving institution; we read them at send time rather than caching them. ↩

  2. Full event schema in the API reference: omniwire.readme.io → Webhooks → payment.settled. ↩

Omniwire team

Engineering, payments and product notes from the people building Omniwire.

Keep reading

Same hub first, then same reader

Liked this? One like it, monthly.

One deep dive, the rail changes that affect your cutoffs, and a changelog digest. Written by engineers, readable by everyone on your team.

We send a confirmation link first (double opt-in). No sales follow-up from subscribing. Unsubscribe in one click. Privacy policy.