Money movement
PaymentsBeta
Railor sends payouts through the provider accounts you connect — Railor never holds funds. Every payment is evaluated against your active policy, routed across eligible providers, submitted with an idempotency key recorded before the call, and tracked to settlement by provider webhooks and reconciliation.
Test mode and live mode
- A
rail_test_…key creates test payments; arail_live_…key creates live ones. They never mix. - Test payments run against a provider's sandbox API when you've connected its sandbox, and against Railor's simulator otherwise.
- Live payments require all of: the deployment's live switch, your workspace approved for live by Railor (with per-payment and daily limits), a production connection, and a provider Railor has approved for live payouts after sandbox evidence.
Quickstart
# 1. Save who you're paying (validated, encrypted, deduplicated)
curl http://localhost:3000/v1/beneficiaries \
-H "Authorization: Bearer rail_test_your_key_here" -H "Content-Type: application/json" \
-d '{"holder_type":"business","holder_name":"Dubai Supplier LLC","country":"AE","currency":"AED",
"method":"iban","details":{"iban":"AE070331234567890123456","bank_name":"Emirates Bank"}}'
# 2. Create the payment: policy check + route plan. Nothing is sent yet.
curl http://localhost:3000/v1/payments \
-H "Authorization: Bearer rail_test_your_key_here" -H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-2041" \
-d '{"beneficiary_id":"ben_…","intent":{"source_entity_country":"IN","source_asset":"USDC",
"source_network":"base","destination_country":"AE","destination_currency":"AED","amount":1000}}'
# 3. Send it
curl -X POST http://localhost:3000/v1/payments/pay_…/submit -H "Authorization: Bearer rail_test_your_key_here"Test keys only ever create test payments. Retrying a create with the same Idempotency-Key returns the original payment.
Payment states
- requires_approval
- Your policy wants an independent approval first (Approvals in the app).
- ready
- Allowed by policy and routed. Nothing has been sent — call submit.
- blocked
- Policy denied it, or no eligible provider can execute it. The reason is on the payment.
- submitting
- Railor is calling the provider right now.
- awaiting_funds
- The provider accepted it and is waiting for the source funds (deposit instructions on the payment).
- processing
- The provider has it and is moving the money.
- completed
- The provider reports the payout settled.
- failed
- Every provider tried said no — definitively. The last rejection is on the payment.
- returned
- It settled and then came back (the beneficiary bank returned it).
- unknown
- A provider call timed out or errored ambiguously. Railor never re-sends this elsewhere; reconciliation asks the provider (replaying the same idempotency key) until it knows.
- cancelled
- Cancelled before any provider had it.
Routing
Eligibility and your policy are gates — a provider that doesn't pass is excluded with its reason. The rest are scored on health 25 · reliability 25 · cost 20 · speed 15 · limits 10 · preference 5 (or the Cheapest / Fastest / Most reliable presets). A dimension with no real data drops out and lowers the reported confidence rather than being guessed. On a definitive rejection Railor tries the next provider; on an ambiguous one it never does. POST /v1/routes shows the plan without creating anything.
Price check
POST /v1/prices (and Price check in the app) answers “send X, how much arrives, through whom?”. Each row carries its basis, because the numbers are not equally strong:
- exact
- A live quote from your own connected account (Wise, Airwallex) — the price you'd actually pay.
- live_public
- A live quote anyone can get from the provider's API (Wise's public quote). Your account's price can differ.
- published
- The provider's published fee schedule (PayZoll, Skydo), applied at the reference rate, with the date it was read and a link to it.
- market_estimate
- Consumer prices Wise's comparison feed collected from other providers' public sites, dated. Opt in with include_market.
Rows are ranked by recipient_amount. A quote missing a cost component (Airwallex's FX quote has no transfer fee in it) is marked cost_complete: false and listed after complete ones; market estimates are listed last and never counted as the best price.
Sandbox scenarios
In test mode, when a provider isn't connected in sandbox, Railor's simulator picks the outcome from the amount's cents:
- .13
- Rejected: insufficient funds — retryable, so routing falls back to the next provider
- .66
- Rejected: compliance — never retried elsewhere
- .55
- Accepted, awaiting funds, then processing, then completed
- .77
- Completed, then returned by the beneficiary bank
- .99
- Outcome unknown (simulated timeout), resolved by reconciliation
- other
- Processing, then completed about 20 seconds later
Webhooks
Add an endpoint under Developers (or POST /v1/webhook_endpoints). Every event is signed; verify before trusting it:
import { createHmac, timingSafeEqual } from "node:crypto"
export function verifyRailor(secret: string, header: string, rawBody: string) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")))
const t = Number(parts.t)
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false // stale → reject
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
return expected.length === parts.v1?.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
// header: request.headers["railor-signature"] → "t=1790000000,v1=5f2…"Events: payment.created, payment.requires_approval, payment.ready, payment.blocked, payment.submitted, payment.awaiting_funds, payment.processing, payment.completed, payment.failed, payment.returned, payment.cancelled, payment.unknown.