railsr-sdk
v1.0.0
Published
Production-grade TypeScript SDK for the Railsr Embedded Finance API
Downloads
151
Maintainers
Readme
railsr-sdk
Production-grade TypeScript SDK for the Railsr Embedded Finance API.
Features
- Full API coverage — Endusers, Ledgers, Transactions, Beneficiaries, Cards, Mandates, Direct Debit, Firewall, Webhooks, Customer
- OAuth 2.0 auto-refresh — transparent
client_credentialstoken management with 60 s pre-emptive refresh - Idempotency — auto-generated
Idempotency-Keyon allPOST/PUT/PATCHrequests - Retry with full-jitter back-off — configurable
maxRetries,baseBackoffMs,maxBackoffMs - Circuit breaker — sliding-window protection against cascading failures
- Client-side rate limiter — token-bucket to stay within Railsr's rate limits
- Telemetry hook — plug in your own metrics / tracing on every HTTP attempt
- Webhook signature verification — constant-time HMAC-SHA256 via
verifyWebhookSignature() - Dual ESM + CJS — works in Node.js, Bun, Deno, and bundled apps
- Zero runtime dependencies — only Node.js built-ins
- Strict TypeScript —
strict,exactOptionalPropertyTypes,noUncheckedIndexedAccess
Requirements
- Node.js ≥ 18 (for native
fetchandcrypto.randomUUID) - TypeScript ≥ 5.x (if using types)
Installation
npm install railsr-sdk
# or
yarn add railsr-sdk
# or
pnpm add railsr-sdkQuick Start
import { Railsr } from "railsr-sdk";
const rails = new Railsr({
clientId: process.env.RAILSR_CLIENT_ID!,
clientSecret: process.env.RAILSR_CLIENT_SECRET!,
environment: "live", // "play" | "play_live" | "live"
});
// 1. Create an enduser
const enduser = await rails.endusers.create({
person: {
name: { family_name: "Smith", given_name: "Alice" },
email: "[email protected]",
date_of_birth: "1990-01-15",
nationality: "GB",
country_of_residence: ["GB"],
},
});
// 2. Open a GBP ledger
const ledger = await rails.ledgers.create({
holder_id: enduser.enduser_id,
ledger_type: "standard-gbp",
asset_class: "currency",
asset_type: "gbp",
});
// 3. Trigger a KYC check
const kyc = await rails.endusers.createKYCCheck(enduser.enduser_id);
// 4. Wait for KYC to pass (polling helper)
const activated = await rails.endusers.waitForStatus(enduser.enduser_id, {
targetStatuses: ["active"],
timeoutMs: 60_000,
});
// 5. Create a beneficiary
const ben = await rails.beneficiaries.create({
name: "ACME Ltd",
currency: "GBP",
uk_sort_code: "010203",
uk_account_number: "12345678",
});
// 6. Send money
const tx = await rails.transactions.sendMoney({
ledger_id: ledger.ledger_id,
beneficiary_id: ben.beneficiary_id,
amount: 10000, // £100.00 in pence
currency: "GBP",
payment_type: "faster-payment",
reference: "Invoice #42",
});
// 7. Wait for the transaction to settle
const settled = await rails.transactions.waitForTerminal(tx.transaction_id);
console.log(settled.status); // "accepted" | "failed" | "quarantined"API Reference
new Railsr(options)
| Option | Type | Default | Description |
|---|---|---|---|
| clientId | string | required | Railsr OAuth 2.0 client ID |
| clientSecret | string | required | Railsr OAuth 2.0 client secret |
| environment | "play" \| "play_live" \| "live" | "play" | Target environment |
| retry | RetryOptions | { maxRetries: 3 } | Retry / back-off config |
| circuitBreaker | CircuitBreakerOptions | { threshold: 5 } | Circuit breaker config |
| rateLimitRps | number | 50 | Client-side RPS cap |
| onRequest | TelemetryHook | noop | Per-attempt telemetry callback |
| signal | AbortSignal | — | Global cancellation signal |
| fetchFn | typeof fetch | globalThis.fetch | Custom fetch implementation |
Endusers — rails.endusers
// CRUD
await rails.endusers.create(params)
await rails.endusers.get(enduserID)
await rails.endusers.list({ status: "active", limit: 50 })
await rails.endusers.update(enduserID, params)
await rails.endusers.patch(enduserID, partialParams)
// KYC
await rails.endusers.createKYCCheck(enduserID)
await rails.endusers.listKYCChecks(enduserID)
await rails.endusers.getKYCCheck(enduserID, checkID)
// Compliance
await rails.endusers.recalculateFirewall(enduserID)
// Polling
await rails.endusers.waitForStatus(enduserID, { targetStatuses: ["active"] })Ledgers — rails.ledgers
await rails.ledgers.create(params)
await rails.ledgers.get(ledgerID)
await rails.ledgers.list({ holder_id: enduserID })
await rails.ledgers.update(ledgerID, { metadata: {} })
await rails.ledgers.findByUKAccount(sortCode, accountNumber)
await rails.ledgers.findByIBAN(iban)
await rails.ledgers.listEntries(ledgerID, { limit: 100 })
await rails.ledgers.creditVirtual({ ledger_id, amount })
await rails.ledgers.debitVirtual({ ledger_id, amount })
await rails.ledgers.devCredit(ledgerID, { amount, currency }) // PLAY only
await rails.ledgers.waitForActive(ledgerID)Transactions — rails.transactions
await rails.transactions.sendMoney(params)
await rails.transactions.interLedger(params)
await rails.transactions.fx(params)
await rails.transactions.get(transactionID)
await rails.transactions.list({ ledger_id, status: "pending" })
await rails.transactions.listQuarantined()
await rails.transactions.approve(transactionID)
await rails.transactions.reject(transactionID, "reason")
await rails.transactions.resolveQuarantine(transactionID, { resolution: "approve" })
await rails.transactions.retry(transactionID)
await rails.transactions.waitForTerminal(transactionID)Beneficiaries — rails.beneficiaries
await rails.beneficiaries.create(params)
await rails.beneficiaries.get(beneficiaryID)
await rails.beneficiaries.list({ enduser_id })
await rails.beneficiaries.update(beneficiaryID, { name })
await rails.beneficiaries.verify(beneficiaryID, { payment_type: "faster-payment" })
await rails.beneficiaries.recalculateFirewall(beneficiaryID)Cards — rails.cards
await rails.cards.create(params)
await rails.cards.get(cardID)
await rails.cards.list({ ledger_id, card_type: "virtual" })
await rails.cards.activate(cardID)
await rails.cards.freeze(cardID)
await rails.cards.unfreeze(cardID)
await rails.cards.cancel(cardID)
await rails.cards.suspend(cardID)
await rails.cards.replace(cardID, { replacement_reason: "lost" })
await rails.cards.getPAN(cardID)
await rails.cards.resetPINAttempts(cardID)
await rails.cards.listTransactions(cardID)
// Spend controls
await rails.cards.createRule(cardID, { rule_type: "amount_limit", limit_amount: 50000 })
await rails.cards.listRules(cardID)
await rails.cards.getRule(cardID, ruleID)
await rails.cards.deleteRule(cardID, ruleID)
// Card Programmes
await rails.cards.listProgrammes()
await rails.cards.getProgramme(programmeID)
// Digital Wallets (Labs)
await rails.cards.createPaymentToken(cardID, params)
await rails.cards.listPaymentTokens(cardID)Mandates & Direct Debit
// Mandates
await rails.mandates.create(params)
await rails.mandates.get(mandateID)
await rails.mandates.list({ enduser_id })
await rails.mandates.cancel(mandateID)
await rails.mandates.waitForActive(mandateID)
// Payments
await rails.payments.create({ mandate_id, amount: 5000 })
await rails.payments.get(paymentID)
await rails.payments.list({ mandate_id })Firewall — rails.firewall
await rails.firewall.setRules({ rules: [...] })
await rails.firewall.getRules()
await rails.firewall.createDataset({ name, columns, rows })
await rails.firewall.listDatasets()
await rails.firewall.updateDataset(name, { columns, rows })
await rails.firewall.deleteDataset(name)
await rails.firewall.getFunctions()Webhooks — rails.webhooks
await rails.webhooks.configure({ url: "https://...", secret: "..." })
await rails.webhooks.getConfig()
await rails.webhooks.listHistory({ from: "2024-01-01", status: "failed" })
await rails.webhooks.retry(notificationID)Customer — rails.customer
await rails.customer.get()
await rails.customer.update({ metadata: {} })
await rails.customer.listProducts()Webhook Signature Verification
import { verifyWebhookSignature } from "railsr-sdk";
// Express example
app.post("/webhook", express.raw({ type: "*/*" }), (req, res) => {
verifyWebhookSignature(
req.body, // Buffer
req.headers["x-railsr-signature"] as string,
process.env.RAILSR_WEBHOOK_SECRET!,
);
// Signature is valid — process the event
const event = JSON.parse(req.body.toString());
console.log(event.type); // e.g. "transaction-accepted"
res.sendStatus(200);
});Throws RailsrError with type: "invalid_signature" on mismatch.
Error Handling
All SDK methods throw RailsrError on failure:
import { RailsrError, isRailsrError } from "railsr-sdk";
try {
await rails.endusers.get("eu_missing");
} catch (err) {
if (err instanceof RailsrError) {
console.log(err.type); // "not_found"
console.log(err.statusCode); // 404
console.log(err.code); // "ERROR_ENTITY_NOT_FOUND"
console.log(err.requestId); // "req_abc123" — for Railsr support
console.log(err.retryable); // false
console.log(err.details); // full response body
}
}Error types
| type | HTTP | Retryable | Meaning |
|---|---|---|---|
| unauthorized | 401 | on first attempt | Invalid / expired credentials |
| forbidden | 403 | ✗ | Insufficient permissions |
| not_found | 404 | ✗ | Resource does not exist |
| conflict | 409 | ✗ | Duplicate / state conflict |
| unprocessable | 422 | ✗ | Validation failure |
| rate_limited | 429 | ✓ | Too many requests |
| server_error | 5xx | ✓ | Railsr internal error |
| circuit_open | — | ✓ | Too many recent failures |
| network | — | ✓ | Connection / DNS error |
| invalid_signature | — | ✗ | Webhook HMAC mismatch |
| validation | — | ✗ | SDK-level validation |
Advanced Configuration
Telemetry / Observability
const rails = new Railsr({
clientId: "...",
clientSecret: "...",
onRequest: (event) => {
// Fires after every HTTP attempt (including retries)
metrics.histogram("railsr.request.duration_ms", event.durationMs, {
method: event.method,
path: event.path,
status: String(event.statusCode ?? 0),
attempt: String(event.attempt),
});
if (event.error) {
metrics.increment("railsr.request.errors", { type: event.error.type });
}
},
});Custom Retry Policy
const rails = new Railsr({
clientId: "...",
clientSecret: "...",
retry: {
maxRetries: 5,
baseBackoffMs: 100,
maxBackoffMs: 30_000,
},
});Circuit Breaker
const rails = new Railsr({
clientId: "...",
clientSecret: "...",
circuitBreaker: {
threshold: 10, // open after 10 failures in the window
resetTimeoutMs: 60_000, // try again after 60 s
},
});Graceful Shutdown
const controller = new AbortController();
const rails = new Railsr({ clientId: "...", clientSecret: "...", signal: controller.signal });
process.on("SIGTERM", () => controller.abort());Per-Request Idempotency Keys
await rails.transactions.sendMoney(params, {
idempotencyKey: "my-own-key-tied-to-db-record-id-42",
});Development
# Install
npm install
# Type-check
npm run typecheck
# Test
npm test
# Test with coverage
npm run test:coverage
# Build
npm run buildEnvironments
| Environment | Base URL | Purpose |
|---|---|---|
| play | https://play.railsbank.com | Sandbox — fake money |
| play_live | https://playlive.railsbank.com | Staging — real rails |
| live | https://live.railsbank.com | Production |
License
MIT © Voidspace
