npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

railsr-sdk

v1.0.0

Published

Production-grade TypeScript SDK for the Railsr Embedded Finance API

Downloads

151

Readme

railsr-sdk

Production-grade TypeScript SDK for the Railsr Embedded Finance API.

npm TypeScript Node.js License: MIT

Features

  • Full API coverage — Endusers, Ledgers, Transactions, Beneficiaries, Cards, Mandates, Direct Debit, Firewall, Webhooks, Customer
  • OAuth 2.0 auto-refresh — transparent client_credentials token management with 60 s pre-emptive refresh
  • Idempotency — auto-generated Idempotency-Key on all POST/PUT/PATCH requests
  • 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 TypeScriptstrict, exactOptionalPropertyTypes, noUncheckedIndexedAccess

Requirements

  • Node.js ≥ 18 (for native fetch and crypto.randomUUID)
  • TypeScript ≥ 5.x (if using types)

Installation

npm install railsr-sdk
# or
yarn add railsr-sdk
# or
pnpm add railsr-sdk

Quick 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 build

Environments

| 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