
Security News
upm Launches as a Fast, Tiny Package Manager Written in TypeScript
upm uses Node.js to deliver fast npm installs in about 250 KB, with a JavaScript API and security defaults.
@finodigital/finoos-sdk
Advanced tools
TypeScript client for the finoOS REST API — full coverage of the published OpenAPI surface
TypeScript client for the finoOS API. It covers every operation in the published OpenAPI spec — authentication, users, banking, manual upload, categorization, contracts, person and company analytics, cockpits, webhooks, company data and platform status.
npm run check:api fails the build when this package and the API diverge.npm install @finodigital/finoos-sdk
The five steps from the agent quick start: authenticate, create a user, connect banking data, track the analysis, read the results.
import {
FinoClient,
createConnectSession,
createUser,
getPersonIncome,
waitForAnalysis,
} from "@finodigital/finoos-sdk";
// 1. Authenticate. The client fetches and refreshes the token itself.
const client = FinoClient.forEnvironment("test", {
clientId: process.env.FINO_CLIENT_ID!,
clientSecret: process.env.FINO_CLIENT_SECRET!,
});
// 2. Create the user every other call is scoped to.
const { user } = await createUser(client, { type: "person", automaticAnalysis: "active" });
const userId = user!.userID!;
// 3. Start a Banking Connect session and send the end user to `redirectURL`.
const session = await createConnectSession(
client,
userId,
{
redirectURL: "https://your.app/success",
errorURL: "https://your.app/error",
exitURL: "https://your.app/exit",
recurring: true,
storeSecrets: true,
},
{ lang: "de" },
);
console.log(session.redirectURL, session.correlationId);
// 4. Wait for the analysis that the completed login triggers.
await waitForAnalysis(client, userId, "person-income", {
correlationId: session.correlationId,
});
// 5. Read the results.
const income = await getPersonIncome(client, userId, { months: 12 });
const client = new FinoClient({
baseUrl: "https://os.test.fino.cloud/api", // or FINO_BASE_URLS.production
clientId: "…",
clientSecret: "…",
tenantId: "my-tenant", // sent as the TenantID header
userIp: "203.0.113.7", // sent as the User-IP header
maxRetries: 3,
timeoutMs: 30_000,
});
FinoClient.forEnvironment("test" | "production", config) picks the base URL for you.
The client authenticates with POST /auth using application/x-www-form-urlencoded, caches the
token, and refreshes it 30 seconds before it expires. Concurrent calls share one authentication.
It retries 429, 5xx and transient network errors with full-jitter backoff, honours Retry-After,
and re-authenticates once on a mid-flight 401.
Every endpoint function takes the same optional last argument:
await disconnectUser(client, userId, {
userIp: "203.0.113.7", // PSD2 requires this on connect, sync, disconnect,
// delete-login and money transfer
tenantId: "other-tenant",
headers: { "X-Trace": "abc" },
signal: AbortSignal.timeout(10_000),
onResponse: (meta) => console.log(meta.status, meta.correlationId, meta.rateLimit),
});
onResponse is how you read the Finoos-Correlation-Id header on any call. The session and
upload helpers return it directly, because the correlation ID is what you track the analysis with.
Prefer webhooks. When you must poll, waitForAnalysis applies the backoff the docs ask for and
stops as soon as your run reports results.
import { getCorrelationStatus, waitForAnalysis } from "@finodigital/finoos-sdk";
// One check: `{ ready: false }` while the run is still going, instead of a thrown 404.
const status = await getCorrelationStatus(client, userId, "categorization");
// Or poll with exponential backoff until your correlation ID comes back.
const correlation = await waitForAnalysis(client, userId, "categorization", {
correlationId: session.correlationId,
timeoutMs: 5 * 60_000,
onPoll: (attempt) => console.log(`poll ${attempt}`),
});
Scopes: banking, categorization, contracts, account-detection, account-history,
renter-information, cockpits, person-* and company-*. See ANALYSIS_SCOPES.
import {
createManagementSession,
getAccounts,
getTransactions,
triggerSynchronization,
} from "@finodigital/finoos-sdk";
const accounts = await getAccounts(client, userId, { includeTransactions: false });
// The endpoint is not paginated. Narrow it with since/until, lastMonths or accountId.
const transactions = await getTransactions(client, userId, {
since: "2026-01-01T00:00:00Z",
accountId: accounts[0]?.accountId,
});
// A manual sync has five documented outcomes, three of which need the end user.
const result = await triggerSynchronization(client, userId, { bankLoginId }, undefined, {
userIp: "203.0.113.7",
});
switch (result.outcome) {
case "synchronized":
break;
case "partial":
console.log(result.login.status);
break;
case "challengeRequired":
case "interfaceDeprecated":
case "unprocessable":
redirect(result.redirectURL);
break;
}
For data you already hold. Create the user with automaticAnalysis: "suspended", upload, then
trigger the analysis yourself.
import { triggerAnalysis, uploadAccounts } from "@finodigital/finoos-sdk";
const { user } = await createUser(client, { type: "company", automaticAnalysis: "suspended" });
await uploadAccounts(client, user!.userID!, { accounts: [/* … */] });
const { correlationID } = await triggerAnalysis(client, user!.userID!);
finoOS echoes the secret you registered inside each delivery — it does not sign them.
import { createWebhook, verifyWebhookSecret } from "@finodigital/finoos-sdk";
import type { FinoWebhookEvent } from "@finodigital/finoos-sdk";
await createWebhook(client, {
webhook: {
callbackURL: "https://your.app/hooks/fino",
events: ["categorization", "person-income"],
httpMethod: "POST",
secret: process.env.FINO_WEBHOOK_SECRET,
},
});
// In your handler:
const event = req.body as FinoWebhookEvent;
if (!verifyWebhookSecret(event, process.env.FINO_WEBHOOK_SECRET!)) return res.sendStatus(401);
res.sendStatus(200); // answer 2xx or finoOS retries up to 5 times
Every failure is a FinoApiError subclass, so one catch covers them all.
| Status | Class |
|---|---|
| 400 | FinoValidationError |
| 401 | FinoAuthError |
| 403 | FinoForbiddenError |
| 404 | FinoNotFoundError |
| 409 | FinoConflictError |
| 422 | FinoUnprocessableError |
| 423 | FinoLockedError |
| 429 | FinoRateLimitError (carries retryAfter and rateLimit) |
| 5xx | FinoServerError |
| — | FinoNetworkError, FinoTimeoutError |
Each one carries status, type, message, the raw body and the correlationId when the API
sent one.
Every operation has a typed function, but you can call the API directly through the same route table the SDK uses:
import { ROUTES } from "@finodigital/finoos-sdk";
const res = await client.request({
route: ROUTES.bankingGetAccounts,
pathParams: { "user-id": userId },
query: { includeTransactions: false },
});
console.log(res.status, res.correlationId, res.data);
npm run check:api # diff the SDK against the live spec
npm run check:api:offline # diff against the vendored copy in openapi/
npm run generate:types # regenerate types and routes from openapi/swagger.json
npm run generate:types:remote # download the live spec first, then regenerate
check:api fails when the SDK declares a route the spec does not have, when a method or path
drifted, when the spec added an operation the SDK does not cover, or when a declared route has no
endpoint function behind it. It runs as part of npm test.
The vendored spec lives in openapi/swagger.json and src/generated/ is produced from it, so
neither is edited by hand. Both are repository-only: the published package carries no copy of
the spec. To check which spec a release was built from, read the exported OPENAPI_VERSION.
npm test is offline and needs no credentials. The end-to-end suite under e2e/ talks to a
real finoOS environment and is opt-in: without configuration every suite skips itself with a
reason.
cp .env.example .env # fill in the client credentials
npm run e2e # runs the suite, then prints the coverage report
npm run e2e:cleanup # lists users a broken run left behind; add -- --delete to remove them
| Suite | What it proves |
|---|---|
01-platform | Auth, token reuse, wrong-credential handling, /ready, category tree, webhooks list, company and logo search |
02-users | Create, read, update, resolve by customUserID, delete, and a 404 afterwards |
03-person-analysis | Upload → trigger → wait → categorization, contracts and every person scope |
04-company-analysis | The same flow against every company scope |
05-banking-upload-crud | Account and transaction writes, connect and management sessions, disconnect, optional demo bank login |
06-webhooks | Register, list, replace, patch, delete, and the secret check a real handler performs |
The flow runs on manual upload, not on a bank login. That needs no browser, no SCA and no
demo environment, and the fixtures in e2e/fixtures.ts generate 14 months of recurring salary,
rent, insurance and loan bookings so the analysis has something real to detect.
Two company scopes stay empty on uploaded data no matter what the fixtures contain:
company-customers-suppliers and company-foreign-sales. They are reported as NO DATA, not
as failures. See the note in e2e/04-company-analysis.e2e.ts for the measurements.
Scopes are assigned by fino per client, and they decide which analytics endpoint answers. The suite classifies every call into four buckets and only the last one fails the run:
OK the endpoint answered
SCOPE MISSING 403 — the client does not have that scope enabled
NO DATA 404 — the scope is on, but the analysis produced nothing
BLOCKED 409 — a precondition stopped the call, e.g. the one-webhook-per-client limit
FAILED anything else
So the first run doubles as a scope audit: it tells you exactly what your client can reach.
These tests are built to run against production, because that is where the test client lives.
createMoneyTransfer is on a
blocklist that __tests__/e2e-guardrails.test.ts enforces on every commit.sdk-e2e- prefixed customUserID and an expiresIn, so anything a crash leaves behind
disappears within a day and npm run e2e:cleanup can find it by prefix.FINOOS_E2E_ALLOW_PRODUCTION=1 on top of FINOOS_E2E=1,
so nobody reaches production by forgetting to set a base URL.| Script | What it does |
|---|---|
npm test | node --test, including the API drift gate and the e2e guard rails |
npm run e2e | The end-to-end suite against a real environment, plus the coverage report |
npm run typecheck | tsc --noEmit |
npm run lint / lint:fix | Biome |
npm run build | ESM bundle plus type declarations |
MIT © Fino Digital GmbH
FAQs
TypeScript client for the finoOS REST API — full coverage of the published OpenAPI surface
We found that @finodigital/finoos-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
upm uses Node.js to deliver fast npm installs in about 250 KB, with a JavaScript API and security defaults.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.