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

bankingcircle-sdk

v1.0.0

Published

A complete, dependency-free TypeScript client for the Banking Circle Connect API: payments, accounts, virtual accounts, FX (incl. WebSocket streaming), reporting, case management, direct debit, ISO20022, and webhooks.

Readme

bankingcircle-sdk

A complete, production-grade, zero-dependency TypeScript client for the Banking Circle Connect API: cross-border payments, accounts, virtual accounts (VIBANs), FX (trading, RFQ, held rates, and live WebSocket streaming), reporting, case management (RFI/recall), direct debit collections, ISO20022 message transport, and webhooks.

This is a TypeScript port of the banking_circle Elixir hex package (and its sibling bankingcircle-go Go module), restructured as an idiomatic, modern npm package.

import { BankingCircleClient } from "bankingcircle-sdk";

Design goals

  • Zero runtime dependencies. Built entirely on platform globals — fetch, WebSocket, crypto.subtle, crypto.randomUUID, FormData/Blob. No supply-chain surface beyond the JS runtime itself.
  • Domain-driven module layout. Each bounded context (payments, accounts, fx, ...) owns its types, validation, and service class in one module, built on a shared kernel in internal/.
  • Client-side validation before any network call. Invalid payment fields, malformed IBANs, etc. throw a ValidationError without hitting the network.
  • Safe retries. Full-jitter exponential backoff, but only for GET/HEAD or requests carrying an Idempotency-Key — a bare POST is never auto-retried, so a flaky network can't duplicate a payment.
  • Structured, classifiable errors. BankingCircleError normalizes every documented Banking Circle error body shape, with an ErrorKind you can branch on.
  • AbortSignal everywhere a network call happens, for cancellation, timeouts, and integration with your own request lifecycle.
  • Dual ESM/CJS with full type declarations — works with import and require() alike.

Install

npm install bankingcircle-sdk

Requires Node.js 18+. FX WebSocket streaming (client.streamFx) requires Node.js 22+ (native WebSocket), or pass a compatible implementation via StreamParams.webSocketImpl on earlier versions. Everything else works in any modern JS runtime — Node, browsers, Deno, Bun, Cloudflare Workers — though see the runtime notes below before using this in a browser.

Quick start

import { BankingCircleClient, BankingCircleError } from "bankingcircle-sdk";

const client = new BankingCircleClient({
  environment: "sandbox",
  username: process.env.BC_USERNAME!,
  password: process.env.BC_PASSWORD!,
  certificateThumbprint: process.env.BC_CERT_THUMBPRINT!,
  requestTimeoutMs: 20_000,
});

try {
  const payment = await client.payments.createSingle({
    debtorAccountId: "acc_123",
    amount: "100.50",
    currency: "EUR",
    creditorName: "Jane Doe",
    creditorIban: "DE89370400440532013000",
    transactionReference: "INV-2026-001",
  });
  console.log("payment created:", payment);
} catch (err) {
  if (err instanceof BankingCircleError) {
    console.error(`payment failed [${err.kind}]: ${err.message}`);
  } else {
    throw err;
  }
}

See examples/basic.ts for a fuller runnable walkthrough (accounts, a payment, and an FX rate lookup) and examples/webhookReceiver.ts for a webhook HTTP handler.

Configuration

const client = new BankingCircleClient({
  environment: "sandbox", // or "production"
  username, password, certificateThumbprint, // required

  requestTimeoutMs: 20_000,     // default 15000
  maxRetries: 3,                 // default 3
  retryBaseDelayMs: 250,         // default 250, capped at 8000

  fetch: myFetch,                 // override the global fetch (mTLS, testing, ...)
  telemetry: myTelemetry,         // see Telemetry below
  webhookEncryptionKey: myKey,    // default key for verifyAndDecryptWebhook
  name: "my-service",             // telemetry label only
});

environment and username/password/certificateThumbprint are required; everything else has a sensible default. The constructor validates configuration and throws immediately — no network call is made during construction, since tokens are fetched lazily and cached.

A client is safe for concurrent use; build one per Banking Circle account/legal-entity credential pair and share it.

mTLS

Banking Circle requires client-certificate authentication at the transport layer in addition to the Basic-auth credentials above. The Fetch API has no standard way to configure this, so pass a pre-configured fetch via the fetch option — for example, with undici:

import { Agent, fetch as undiciFetch } from "undici";
import fs from "node:fs";

const agent = new Agent({
  connect: {
    cert: fs.readFileSync("client-cert.pem"),
    key: fs.readFileSync("client-key.pem"),
  },
});

const client = new BankingCircleClient({
  // ...
  fetch: ((url, init) => undiciFetch(url, { ...init, dispatcher: agent })) as typeof fetch,
});

Services

Every bounded context is exposed as a property on BankingCircleClient, and documented in its own module:

| Property | Module | Covers | |-----------------------------|----------------------|---| | client.payments | payments | Single & bulk payments, status, cancellation, MT103, recalls, traces, Correspondent/Agency Banking (FI-to-FI) | | client.accounts | accounts | Listing, balances, bookings, Account Holder Verification | | client.virtualAccounts | virtualAccounts | VIBAN listing, ordering, customer/UBO details, closure | | client.fx | fx | Market order / RFQ / held-rate trading, indicative rates, trade history, WebSocket streaming (client.streamFx) | | client.reporting | reporting | Async report request/poll/download, sync reconciliation report | | client.cases | cases | RFI/Recall case listing, detail, attachments, answers | | client.directDebit | directDebit | Mandate-referenced collection initiation (idempotency-key supported) | | client.iso20022 | iso20022 | pain.001/pacs.008 XML transport, camt.053 statements | | client.webhooks | webhooks | Subscription CRUD, sandbox simulation | | — | verifyAndDecryptWebhook (top-level export) | Network-free AES-256-GCM payload verification/decryption |

Payments

// Single payment
const payment = await client.payments.createSingle({
  debtorAccountId: "acc_123",
  amount: "100.50",
  currency: "EUR",
  creditorName: "Jane Doe",
  creditorIban: "DE89370400440532013000",
  transactionReference: "INV-2026-001",
  urgency: "instant", // optional, defaults to "standard"
});

// Bulk payment — every row validated client-side; a single invalid row
// rejects the whole batch before any network call, with 1-based row
// indices matching Banking Circle's elementIndex error semantics.
import { BulkValidationError } from "bankingcircle-sdk";

try {
  await client.payments.createBulk([
    { debtorAccountId: "acc_123", amount: "10.00", currency: "EUR", /* ... */ },
    { debtorAccountId: "acc_123", amount: "20.00", currency: "EUR", /* ... */ },
  ]);
} catch (err) {
  if (err instanceof BulkValidationError) {
    for (const row of err.rows) {
      console.error(`row ${row.index}:`, row.error.fields);
    }
  }
}

// Recall / trace a payment
await client.payments.initiateRecall(paymentId, "AM09"); // wrong amount
await client.payments.initiateTrace(paymentId);

Only directDebit.initiate currently carries documented idempotency-key support — createSingle/createBulk are never auto-retried by the shared HTTP pipeline for exactly that reason. If a payment request times out, check getByTransactionReference before resubmitting.

FX — REST and streaming

// Market order
const trade = await client.fx.trade({
  clientOrderId: "order-1",
  buyCurrency: "USD",
  sellCurrency: "EUR",
  amount: "10000",
  amountCurrency: "EUR",
  tenor: "SPOT",
});

// RFQ -> trade
const [quote] = await client.fx.requestQuotes([
  { currencyOne: "EUR", currencyTwo: "USD", amount: "10000", amountCurrency: "EUR", tenor: "SPOT", requestType: "Rfq" },
]);
await client.fx.trade({
  clientOrderId: "order-2", buyCurrency: "USD", sellCurrency: "EUR",
  amount: "10000", amountCurrency: "EUR", quoteId: quote["id"] as string,
});

// Live streaming quotes + Market Order execution (requires Node 22+, see Install)
const stream = await client.streamFx({
  customerId: "000012345",
  onMessage: (msg) => console.log(msg.type(), msg),
});
stream.subscribe("EUR/USD", "SPOT");
stream.marketOrder({
  clientOrderId: "stream-order-1",
  buyCurrency: "EUR", sellCurrency: "USD",
  amountCurrency: "EUR", amount: "3000000", tenor: "SPOT",
});
// ... later
stream.close();

Reporting

// Blocking convenience wrapper around request -> poll -> download
const body = await client.reporting.fetchReport(
  "reconciliation",
  { fromDate: "2026-07-01", toDate: "2026-07-27" },
  { timeoutMs: 3 * 60_000 },
);

// Or drive the flow yourself (e.g. from a background job)
const requestId = await client.reporting.requestReport("account-activity", attrs);
const outcome = await client.reporting.pollStatus(requestId);
if (outcome.complete && outcome.reportId) {
  const body = await client.reporting.download(outcome.reportId);
}

Node.js required for the async flow. requestReport/pollStatus read the real HTTP status and Location header off a manually-followed redirect. Browsers make this response opaque per the Fetch spec; Node's native fetch does not. Since this API needs server-held OAuth2 credentials that shouldn't reach a browser anyway, this is a non-issue for the intended backend-to-backend usage.

Webhooks

import { verifyAndDecryptWebhook } from "bankingcircle-sdk";

// Subscribe
await client.webhooks.createSubscription({
  url: "https://example.com/webhooks/banking-circle",
  encryptionKey: myThirtyTwoCharacterKey,
  eventTypes: ["PaymentProcessed", "CaseOpened"],
});

// Verify + decrypt an inbound payload (network-free — call this from
// your own HTTP handler; see examples/webhookReceiver.ts)
const event = await verifyAndDecryptWebhook(rawBodyBytes, {
  checksum: req.headers["x-bc-checksum"],
  tag: req.headers["x-bc-auth-tag"],
  nonce: req.headers["x-bc-nonce"],
  key: myThirtyTwoCharacterKey,
});

Error handling

Every failure mode — transport errors, HTTP 4xx/5xx, auth failures, and client-side validation — surfaces as a BankingCircleError:

import { BankingCircleError } from "bankingcircle-sdk";

try {
  await client.payments.createSingle(input);
} catch (err) {
  if (err instanceof BankingCircleError) {
    switch (err.kind) {
      case "validation_error":
        // client-side or 422 validation problem; see err.details
        break;
      case "rate_limited":
        // 429; err.retryAfterMs is populated if the server sent one
        break;
      case "auth_error":
        // 401/403
        break;
    }
    if (err.retryable) {
      // "rate_limited", "server_error", "timeout", "transport_error"
    }
  }
}

err.details normalizes both of Banking Circle's documented error body shapes (the single-object propertyName/errorCode/errorDescription shape, and the bulk-operation fieldIndex/elementIndex list shape) into one ErrorDetail[].

Telemetry

Implement the Telemetry interface to receive lifecycle events for every outgoing request (across every service):

interface Telemetry {
  onRequestStart(method: string, path: string): void;
  onRequestStop(method: string, path: string, durationMs: number, status: number | undefined, error: unknown): void;
}

Wire it up via new BankingCircleClient({ telemetry: myImpl, ... }). Use it to feed OpenTelemetry, Prometheus, or structured logging.

Runtime notes

  • Node.js is the intended runtime. This client holds long-lived OAuth2 client credentials — the kind of secret that should never ship to a browser. Browser support is provided where it costs nothing (the code has no Node-specific imports), but the reporting service's async flow and the FX WebSocket's header-based auth (see below) both have browser-specific caveats documented in their respective modules.
  • FX streaming (client.streamFx) authenticates via a ?bearerToken= query parameter, because the standard WebSocket constructor has no mechanism for custom handshake headers. This is a common workaround for that exact constraint but is not confirmed against Banking Circle's documentation for this specific endpoint — verify it against your sandbox, and see FxStream's doc comment for the fallback if it doesn't work.

Project structure

bankingcircle-sdk/
├── src/
│   ├── index.ts             # Public API barrel export
│   ├── client.ts             # BankingCircleClient facade
│   ├── config.ts             # Options validation/resolution
│   ├── environment.ts        # Sandbox/Production host resolution
│   ├── errors.ts, telemetry.ts, json.ts
│   ├── internal/
│   │   ├── auth/               # Cached, single-flight-refreshed OAuth2 TokenManager
│   │   └── http/                # Shared request pipeline: retries, idempotency, telemetry
│   ├── payments/               # Types + validation + CSV + service, one module per bounded context
│   ├── accounts/
│   ├── virtualAccounts/
│   ├── fx/                     # + stream.ts for WebSocket quote streaming / Market Orders
│   ├── reporting/
│   ├── cases/
│   ├── directDebit/
│   ├── iso20022/
│   ├── webhooks/               # Subscription management (network)
│   └── webhook/                # Payload verification/decryption (network-free)
├── test/                      # vitest — unit + integration tests against local HTTP servers
└── examples/
    ├── basic.ts
    └── webhookReceiver.ts

Each bounded-context module is self-contained: its types, client-side validation, and service class live together, on top of the shared kernel in internal/. internal/ modules are never part of the public API surface (see src/index.ts's barrel export).

Confidence notes / scope

This SDK implements the full documented Banking Circle Connect API surface, with two exceptions, both explained in src/index.ts's module doc comment:

  • Correspondent/Agency Banking over raw SWIFT FIN (MT101/MT103 message exchange) is not an HTTP endpoint and is out of scope.
  • Aliases (PayID, etc.) are described in Banking Circle's docs but no REST endpoint paths/payload shapes are published anywhere we could find — inventing plausible-looking ones for a payment-routing feature would be actively dangerous, not just inconvenient.

Additionally:

  • VirtualAccountsService.order's exact endpoint path/payload shape is inferred from documentation terminology rather than confirmed directly against the API reference — see that class's doc comment.
  • FxStream's query-param WebSocket auth (see Runtime notes).
  • ReportingService's async flow requires a Node-like fetch (see Reporting).

Everything else has been checked against the reference Elixir/Go clients' documented behavior.

Development

npm run build          # tsup -> dist/ (ESM + CJS + .d.ts)
npm run typecheck       # tsc --noEmit across src + test + examples
npm run lint             # eslint src test examples
npm run format:check     # prettier --check
npm test                 # vitest run
npm run test:coverage    # vitest run --coverage
npm run example:basic    # tsx examples/basic.ts
npm run example:webhook  # tsx examples/webhookReceiver.ts

License

MIT — see LICENSE.