moneyhub-sdk
v1.0.0
Published
Production-grade TypeScript SDK for the Moneyhub Open Finance API: account aggregation (AIS), payments (PIS), OpenID Connect authentication, and webhook verification.
Downloads
29
Maintainers
Readme
moneyhub-sdk
Production-grade TypeScript SDK for the Moneyhub Open Finance API: account aggregation (AIS), payment initiation (PIS), OpenID Connect authentication, and webhook verification — all in one zero-dependency package.
Contents
- Features
- Requirements
- Installation
- Quick start
- Configuration
- Authentication
- Account aggregation (AIS)
- Payment initiation (PIS)
- Webhooks
- Error handling
- Domain services reference
- Advanced usage
- Development
Features
- Complete API coverage — 35 domain services mirroring every resource group in the Moneyhub V3 API
- Full OIDC support — PAR, authorisation URL building, code exchange, client credentials, refresh,
private_key_jwtassertions, id_token + webhook JWT verification - Retry / back-off — configurable exponential back-off with jitter on 5xx responses,
Retry-After-aware 429 handling, per-requestAbortSignalpropagation - Structured errors —
MoneyhubErrorwith discriminatedreason,status,code,retryAfter,isRetryable, and narrow type guards (isApiError,isRateLimitedError, …) - Zero runtime dependencies — the only required APIs are
fetch(Node 18+, browsers, Deno, Bun, CF Workers) and Web CryptoSubtleCrypto(same runtime coverage) - Dual ESM + CJS build — full
exportsmap, tree-shakeable, works in every module system - TypeScript-first — strict types throughout, source maps, declaration maps
Requirements
- Node.js 18.17.0 or later (for global
fetchandcrypto.subtle) - TypeScript 5.0+ (if using from TypeScript source)
Browsers, Deno, Bun, and Cloudflare Workers are supported wherever fetch and crypto.subtle are available.
Installation
npm install moneyhub-sdk
# or
pnpm add moneyhub-sdk
# or
yarn add moneyhub-sdkQuick start
import { MoneyhubClient, createConfig, MoneyhubClaims, aisScopes } from "moneyhub-sdk";
// 1. Build a config once (typically at app startup).
const config = createConfig("your-client-id", "production", {
privateKey: await crypto.subtle.importKey(
"pkcs8",
pkcs8DerBytes, // your RSA private key as a DER/PKCS#8 ArrayBuffer
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["sign"],
),
keyId: "your-key-id", // the kid registered with Moneyhub
redirectUri: "https://yourapp.example.com/callback",
});
// 2. Instantiate the client (cheap — no network calls at construction time).
const moneyhub = new MoneyhubClient(config);
// 3. Start an AIS (account aggregation) flow with PAR.
const { url } = await moneyhub.auth.pushAuthorisationRequest({
scope: aisScopes(), // "openid accounts:read transactions:read"
claims: new MoneyhubClaims().putSub(""), // empty sub = register a new Moneyhub user
state: crypto.randomUUID(),
});
// Redirect the user to `url` …
// 4. After redirect back, exchange the code.
const tokens = await moneyhub.auth.exchangeCode(searchParams.get("code")!);
// 5. Use the access token to read accounts.
const accounts = await moneyhub.accounts.list(tokens.accessToken);
console.log(accounts);Configuration
import { createConfig, importPrivateKeyPem } from "moneyhub-sdk";
const config = createConfig(
"your-client-id", // required
"production", // "production" | "sandbox"
{
// Key material — required for private_key_jwt (default authMethod).
privateKey: await importPrivateKeyPem(fs.readFileSync("private.pem", "utf-8")),
keyId: "key-1",
// Optional OAuth redirect target.
redirectUri: "https://yourapp.example.com/callback",
// Override base URLs (e.g. for a dedicated sandbox host).
identityUrl: "https://identity.moneyhub.co.uk",
apiUrl: "https://api.moneyhub.co.uk/v3.0",
// Use HTTP Basic instead of private_key_jwt (sandbox / early dev only).
// authMethod: "client_secret_basic",
// clientSecret: "your-client-secret",
// Fetch/timeout tuning.
fetch: customFetch, // inject a custom fetch for logging or proxies
timeoutMs: 30_000, // per-request timeout (default 60 s)
maxRetries: 3, // retries on 429/5xx (default 2)
maxRetryAfterMs: 15_000, // cap on Retry-After driven sleeps (default 30 s)
},
);createConfig throws a MoneyhubError with reason: "config_error" immediately if the resulting configuration is invalid (missing key, unknown environment, etc.).
Authentication
Pushed Authorisation Requests (PAR)
PAR keeps the claims payload off the browser URL bar entirely. Prefer it in production:
import { MoneyhubClaims, aisOfflineScopes } from "moneyhub-sdk";
const { url, requestUri, expiresIn } = await moneyhub.auth.pushAuthorisationRequest({
scope: aisOfflineScopes(),
claims: new MoneyhubClaims()
.putSub("existing-user-id") // omit or pass "" for new users
.putCategoryType("personal"),
state: sessionState,
});
// Redirect the user's browser to `url`.
res.redirect(url);Building an authorisation URL inline
For simpler use cases (no large claims payload):
const url = await moneyhub.auth.buildAuthorisationUrl({
scope: "openid accounts:read",
state: sessionState,
});Exchanging a code for tokens
const tokens = await moneyhub.auth.exchangeCode(
req.query.code,
"https://yourapp.example.com/callback", // override redirectUri for this call
);
// tokens: { accessToken, idToken?, refreshToken?, tokenType, expiresIn?, scope? }Client Credentials tokens
For server-to-server calls on behalf of a known user, or for application-level endpoints (discovery, catalog):
// Scoped to a specific user — used to read their data.
const userToken = await moneyhub.auth.tokenForUser(userId, "accounts:read transactions:read");
// Unscoped application token — used for catalog/discovery endpoints.
const appToken = await moneyhub.auth.tokenForUser();Refresh tokens
const fresh = await moneyhub.auth.refreshToken(tokens.refreshToken);Verifying id_tokens
const claims = await moneyhub.auth.verifyIdToken(tokens.idToken);
// claims["sub"] is the user's Moneyhub userId.
const userId = claims["sub"] as string;MoneyhubClaims builder
import { MoneyhubClaims } from "moneyhub-sdk";
const claims = new MoneyhubClaims()
.putSub("user-123")
.putCategoryType("personal")
.putConnectionId("conn-456") // for re-auth of a specific connection
.putPayment({ amount: 5000, ... }); // for PIS consent
// Serialise for embedding in an authorisation request.
const json = claims.toString();Account aggregation (AIS)
// List all connected accounts.
const accounts = await moneyhub.accounts.list(token, { accountType: "cash:current" });
// Fetch a single account.
const account = await moneyhub.accounts.get(token, accountId);
// Historical balance snapshots.
const balances = await moneyhub.accounts.balances(token, accountId, {
fromDate: "2024-01-01",
toDate: "2024-06-30",
});
// List transactions.
const transactions = await moneyhub.transactions.list(token, {
accountId,
fromDate: "2024-01-01",
limit: 100,
});
// Correct a transaction's category.
await moneyhub.transactions.updateCategory(token, transactionId, categoryId);
// Split a transaction.
await moneyhub.transactions.split(token, transactionId, [
{ amount: 3500, category: "groceries" },
{ amount: 1500, category: "household" },
]);
// Spending analysis (aggregated stats).
const analysis = await moneyhub.spendingAnalysis.get(token, {
fromDate: "2024-01-01",
toDate: "2024-01-31",
categoryType: "personal",
});Payment initiation (PIS)
Single Immediate Payment
import { MoneyhubClaims, paymentScopes } from "moneyhub-sdk";
// 1. Create a payee.
const payee = await moneyhub.payees.create(appToken, {
name: "Acme Ltd",
accountIdentifications: [{ type: "SortCodeAccountNumber", identification: "60161300012345" }],
});
// 2. Start the payment auth flow.
const { url } = await moneyhub.auth.pushAuthorisationRequest({
scope: paymentScopes(),
claims: new MoneyhubClaims()
.putSub(userId)
.putPayment({
payeeId: payee.id,
amount: 10000, // in pence
currency: "GBP",
payeeType: "registered",
reference: "INV-001",
}),
});
res.redirect(url);
// 3. After redirect, exchange the code and poll for status.
const tokens = await moneyhub.auth.exchangeCode(code);
const claims = await moneyhub.auth.verifyIdToken(tokens.idToken!);
const paymentId = (claims["mh:payment"] as { paymentId: string }).paymentId;
// 4. Poll.
const payment = await moneyhub.payments.status(tokens.accessToken, paymentId);Variable Recurring Payments (VRP)
// Trigger a sweep against an established VRP consent.
const sweep = await moneyhub.recurringPayments.sweep(vrpToken, consentId, {
amount: 25000,
reference: "Monthly savings sweep",
});
// Check available funds without triggering a payment.
const available = await moneyhub.recurringPayments.confirmFunds(vrpToken, consentId, {
amount: 25000,
});Standing orders
// View a standing order created via PIS.
const so = await moneyhub.standingOrders.get(token, standingOrderId);
// Cancel it.
await moneyhub.standingOrders.cancel(token, standingOrderId);Pay Links
// Create a shareable, hosted payment link.
const link = await moneyhub.payLinks.create(appToken, {
amount: 5000,
currency: "GBP",
reference: "Invoice 42",
payeeId: payee.id,
});
// Share link.url with the payer.
console.log(link.url);Webhooks
import { WebhooksService } from "moneyhub-sdk";
// Share the auth service's JWKS cache when using MoneyhubClient — this
// is done automatically; shown here only for standalone usage.
const verifier = new WebhooksService(
config.identityUrl,
fetch,
moneyhub.auth.jwksCache, // optional; if omitted a fresh cache is created
);
// In your webhook endpoint handler (Express example):
app.post("/webhook", express.raw({ type: "*/*" }), async (req, res) => {
// Acknowledge immediately — Moneyhub has a 5-second response timeout.
res.sendStatus(200);
try {
const event = await verifier.parse(req.body); // Buffer or string
// event.id — e.g. "newTransactions", "paymentCompleted"
// event.userId — Moneyhub user id
// event.payload — event-specific fields
// event.raw — full decoded payload
switch (event.id) {
case "newTransactions":
await handleNewTransactions(event.userId!, event.payload);
break;
case "paymentCompleted":
await handlePaymentCompleted(event.payload);
break;
}
} catch (err) {
console.error("webhook verification failed", err);
}
});WebhooksService.parse automatically detects whether the incoming body is a plain JSON object or a compact JWS (signed JWT delivery), and verifies the signature against Moneyhub's JWKS for the latter.
Error handling
Every function that can fail throws a MoneyhubError. Use the reason discriminant and type guards to branch:
import {
isMoneyhubError,
isRateLimitedError,
isApiError,
isNetworkError,
} from "moneyhub-sdk";
try {
const accounts = await moneyhub.accounts.list(token);
} catch (err) {
if (isRateLimitedError(err)) {
// err.retryAfter is the Retry-After header value in seconds (when present).
await sleep((err.retryAfter ?? 5) * 1000);
return retry();
}
if (isNetworkError(err)) {
// Transient connectivity failure — safe to retry with back-off.
return retry();
}
if (isApiError(err)) {
// err.status — HTTP status code (4xx or 5xx)
// err.code — Moneyhub error code string, e.g. "INVALID_REQUEST"
// err.body — raw decoded response body
console.error(`API error ${err.status} (${err.code ?? "unknown"})`);
return;
}
throw err;
}err.isRetryable is true for rate_limited, network_error, and api_error with a 5xx status — matching the conditions the built-in retry logic already handles automatically.
err.toJSON() returns a plain, serialisable object safe to pass to an error tracker or structured log.
Error reasons
| reason | When thrown |
|---------------------|-----------------------------------------------------------------------|
| config_error | createConfig called with invalid or missing parameters |
| validation_error | A required parameter was missing or out of range |
| network_error | The fetch call threw (DNS failure, TCP reset, timeout, etc.) |
| api_error | Moneyhub returned a non-2xx response (4xx other than 429, or 5xx) |
| rate_limited | Moneyhub returned 429 Too Many Requests |
| decode_error | The response or webhook body could not be decoded as expected JSON |
| jwt_error | A JWS/JWT could not be verified (bad signature, expired, missing kid) |
Domain services reference
| Property on MoneyhubClient | Service class | Summary |
|------------------------------------|----------------------------------------|-----------------------------------------------|
| auth | AuthService | OIDC flows, PAR, tokens, id_token verification |
| accounts | AccountsService | Accounts, balances, AIS standing orders |
| affordability | AffordabilityService | Affordability / income-verification reports |
| authRequests | AuthRequestsService | Hosted auth request URL generation |
| bankIcons | BankIconsService | Bank/institution icon images |
| beneficiaries | BeneficiariesService | Payees detected from transaction history |
| categories | CategoriesService | Category taxonomy, categorisation-as-a-service |
| connections | ConnectionsService | Bank connection lifecycle + catalog |
| consentHistory | ConsentHistoryService | Consent audit trail |
| counterparties | CounterpartiesService | Merchants/payees behind transactions |
| discovery | DiscoveryService | OIDC discovery document |
| globalCounterparties | GlobalCounterpartiesService | Global merchant reference database |
| holdings | HoldingsService | Investment holdings, ISIN matching |
| notificationThresholds | NotificationThresholdsService | Balance alert thresholds (VRP triggers) |
| payees | PayeesService | Creditor accounts for PIS |
| payFile | PayFileService | Bulk/batch payment files |
| payLinks | PayLinksService | Shareable hosted payment links |
| payments | PaymentsService | Single Immediate Payment status + refunds |
| projects | ProjectsService | User-defined account groupings |
| recurringPayments | RecurringPaymentsService | VRP consents + sweeps |
| regularTransactions | RegularTransactionsService | Detected recurring transaction series |
| rentalRecords | RentalRecordsService | Rent payment credit reporting |
| resellerCheck | ResellerCheckService | Reseller/partner validation |
| savingsGoals | SavingsGoalsService | Savings progress goals |
| scimUsers | ScimUsersService | SCIM user identity (widget provisioning) |
| spendingAnalysis | SpendingAnalysisService | Aggregated category spending stats |
| spendingGoals | SpendingGoalsService | Budget/spending goals |
| standardFinancialStatements | StandardFinancialStatementsService | SFS reports (lending/collections) |
| standingOrders | StandingOrdersService | PIS standing orders |
| statements | StatementsService | AIS account statements |
| tax | TaxService | SA105 / property income data |
| transactions | TransactionsService | Transactions, splits, file attachments |
| users | UsersService | User CRUD, user-scoped connections |
| webhooks | WebhooksService | Webhook parsing + JWS verification |
Advanced usage
Injecting a custom fetch
import { createConfig } from "moneyhub-sdk";
// Example: wrap fetch with structured logging.
const loggingFetch: typeof fetch = async (input, init) => {
const start = Date.now();
const res = await fetch(input, init);
console.log(`${String(init?.method ?? "GET")} ${String(input)} → ${res.status} (${Date.now() - start}ms)`);
return res;
};
const config = createConfig("client-id", "production", {
privateKey: key,
keyId: "k1",
fetch: loggingFetch,
});Using individual services without MoneyhubClient
All domain services can be constructed directly from a MoneyhubConfig:
import { AccountsService, createConfig } from "moneyhub-sdk";
const config = createConfig(...);
const accounts = new AccountsService(config);
const list = await accounts.list(token);Key import helper
import { importPrivateKeyPem } from "moneyhub-sdk";
import { readFileSync } from "node:fs";
const key = await importPrivateKeyPem(readFileSync("private-key.pem", "utf-8"));Development
# Install dependencies
npm install
# Type-check
npm run typecheck
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build (ESM + CJS + .d.ts)
npm run build
# Lint
npm run lint
# Format
npm run formatProject structure
src/
core/
config.ts — createConfig, MoneyhubConfig
errors.ts — MoneyhubError, type guards
http-client.ts — HttpClient, retry/backoff, MoneyhubResponse
base-service.ts — BaseService, shared helpers
auth/
auth-service.ts — AuthService (OIDC flows)
claims.ts — MoneyhubClaims builder
id-token.ts — JWS/id_token verification
jwks.ts — JwksCache, JWK type
jwt.ts — RS256 signing, importPrivateKeyPem
scopes.ts — Scopes constants, scope helpers
domains/
accounts.ts — AccountsService
… (35 domain services)
client.ts — MoneyhubClient facade
index.ts — Public API surface
test/
sdk.test.ts — 44 tests covering errors, config, HTTP, auth, webhooksLicense
MIT © 2025 Kanishka
