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.
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
-
Participant limits are set by the network and by each receiving institution; we read them at send time rather than caching them. ↩
-
Full event schema in the API reference: omniwire.readme.io → Webhooks →
payment.settled. ↩