starlingbank-sdk
v1.0.1
Published
Complete, production-grade TypeScript/JavaScript SDK for the Starling Bank Public API (accounts, balances, payments, cards, savings goals, transactions, OAuth2, webhooks, and more).
Maintainers
Readme
starlingbank-sdk
A complete, dependency-free TypeScript SDK for the Starling Bank Public API — accounts, balances, cards, payments, payees, savings goals, direct debits, transactions, merchants, receipts, webhooks, customer onboarding, and OAuth2, all fully typed.
Ported and modernized from Voidspace's starling_bank (Elixir/Hex) and starlingbank-go (Go) packages, covering the same complete API surface.
- Zero runtime dependencies — built on native
fetch/AbortController. - Dual ESM + CJS builds, with full
.d.tstype declarations. - Automatic retry with exponential backoff + jitter for network errors and
429/5xx, safe-by-default (never retries non-idempotent payment/transfer calls). - Per-request timeouts via
AbortController. - One normalized
StarlingErrorfor every failure mode (HTTP, network, timeout, decode). - Webhook HMAC-SHA512 signature verification built in.
- Lifecycle hooks for logging/metrics/tracing without a hard dependency on any library.
- Node.js 18+. (Browser use is not recommended — see Security.)
Install
npm install starlingbank-sdkQuick start
import { StarlingClient, Money } from "starlingbank-sdk";
const client = new StarlingClient({
accessToken: process.env.STARLING_ACCESS_TOKEN,
environment: "sandbox", // or "production"
});
const accounts = await client.accounts.list();
const balance = await client.balances.get(accounts[0].accountUid);
console.log(Money.format(balance.availableToSpend)); // "£1,234.56"Resources
Method coverage is the union of both source packages (starling_bank Elixir + starlingbank-go), audited method-by-method:
| Resource | Methods |
| --- | --- |
| client.accounts | list(), get(uid), identifiers(uid), confirmationOfFunds(uid, minorUnits) |
| client.accountHolders | type(), name(), individual(), joint(), business(), customer(), address(), updateAddress(), updateEmail(), authorisingIndividual() |
| client.balances | get(accountUid) |
| client.cards | list(), enable(), disable(), controls(), setSpendingLimit(), removeSpendingLimit(), setChannelControl() |
| client.directDebits | list(), get(uid), delete(uid) |
| client.merchants | get(uid), getLocation(merchantUid, locationUid) |
| client.oauth | authorizeUrl(), exchangeCode(), refreshToken() |
| client.onboarding | create(), status(uid) (requires elevated TPP access) |
| client.payees | list(), create(), delete(uid), image(uid) |
| client.payments | payLocalOnce(), createLocalPayment(), listLocal(), listScheduledPayments(), payInternational(), internationalQuote(), listStandingOrders(), getStandingOrder(), getNextPaymentDates(), putStandingOrder(), deleteStandingOrder() |
| client.receipts | create(), get(uid), attachToTransaction(), listForTransaction(), uploadAttachment(), getAttachment() |
| client.savingsGoals | list(), get(), create(), delete(), addMoney(), withdrawMoney(), setPhoto(), getRecurringTransfer(), setRecurringTransfer(), deleteRecurringTransfer() |
| client.transactions | list(), get(), updateMetadata(), updateSpendingCategory(), updateUserNote(), listAttachments(), attachReceipt(), attachment(), getAttachment(), statementPdf(), statementPdfForPeriod(), statementCsv() |
| client.webhooks | list(), register(), delete(), plus the standalone verifyWebhookSignature() |
67 methods across 14 resources. Every method is documented with JSDoc/TSDoc (visible in your editor) describing required OAuth scopes and payload shapes.
OAuth2 flow
const client = new StarlingClient({ environment: "sandbox" });
const authorizeUrl = client.oauth.authorizeUrl({
clientId,
redirectUri: "https://your-app.com/callback",
scope: ["account:read", "balance:read", "transaction:read"],
state: crypto.randomUUID(),
});
// redirect the user to `authorizeUrl`, then on the callback:
const tokens = await client.oauth.exchangeCode({ code, clientId, clientSecret, redirectUri });
const authedClient = client.withAccessToken(tokens.access_token);Money helpers
Starling represents money as { currency, minorUnits }. The Money helper avoids float arithmetic mistakes:
import { Money } from "starlingbank-sdk";
Money.of(12.5, "GBP"); // { currency: "GBP", minorUnits: 1250 }
Money.toMajorUnits({ currency: "GBP", minorUnits: 1250 }); // 12.5
Money.format({ currency: "GBP", minorUnits: 1250 }); // "£12.50"Error handling
import { StarlingError } from "starlingbank-sdk";
try {
await client.accounts.get("unknown-uid");
} catch (err) {
if (err instanceof StarlingError) {
console.error(err.reason, err.status, err.message, err.requestId);
// reason: "unauthorized" | "forbidden" | "insufficient_scope" | "not_found"
// | "bad_request" | "rate_limited" | "conflict" | "server_error"
// | "network_error" | "timeout_error" | "decode_error" | "unknown"
}
}Retries and timeouts
const client = new StarlingClient({
accessToken,
timeoutMs: 10_000,
retry: { maxAttempts: 4, baseDelayMs: 200, maxDelayMs: 3000 },
});Retries only apply to safe/idempotent requests (GET, DELETE, and explicitly-idempotent transfer calls keyed by a transferUid you supply). One-off payment calls like payments.payLocalOnce() are never auto-retried, to avoid double-spending on a flaky connection.
Lifecycle hooks (logging / metrics / tracing)
const client = new StarlingClient({
accessToken,
hooks: [
(phase, event) => {
if (phase === "stop") console.log(`${event.method} ${event.path} — ${event.durationMs}ms`);
if (phase === "exception") console.error(`${event.method} ${event.path} failed`, event.error);
},
],
});Webhook signature verification
import { verifyWebhookSignature } from "starlingbank-sdk";
import express from "express";
const app = express();
app.post("/webhooks/starling", express.raw({ type: "*/*" }), (req, res) => {
const valid = verifyWebhookSignature({
rawBody: req.body, // must be the raw, unparsed Buffer/string
signature: req.header("X-Hook-Signature") ?? "",
secret: process.env.STARLING_WEBHOOK_SECRET!,
});
if (!valid) return res.status(401).end();
res.status(200).end();
});Multi-tenant servers
const base = new StarlingClient({ environment: "production", retry: { maxAttempts: 4, baseDelayMs: 200, maxDelayMs: 3000 } });
function clientFor(userAccessToken: string) {
return base.withAccessToken(userAccessToken);
}Security
- This SDK is designed for server-side use. Never ship a real Starling access token or webhook secret to a browser.
verifyWebhookSignature()uses Node'snode:cryptoand therefore only runs in Node.js (or compatible server runtimes).- All amounts are handled in integer minor units end-to-end to avoid floating-point rounding errors.
Development
npm install
npm run typecheck
npm run lint
npm test
npm run buildLicense
MIT
