New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@finodigital/finoos-sdk

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@finodigital/finoos-sdk

TypeScript client for the finoOS REST API — full coverage of the published OpenAPI surface

latest
Source
npmnpm
Version
2.0.0
Version published
Maintainers
1
Created
Source

@finodigital/finoos-sdk

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.

  • ESM only, Node 22+, zero runtime dependencies.
  • Types generated straight from the spec, so they cannot drift by hand.
  • npm run check:api fails the build when this package and the API diverge.

Install

npm install @finodigital/finoos-sdk

Quick start

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 });

Client

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.

Per-call options

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.

Analysis lifecycle

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.

Banking

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;
}

Manual upload

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!);

Webhooks

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

Errors

Every failure is a FinoApiError subclass, so one catch covers them all.

StatusClass
400FinoValidationError
401FinoAuthError
403FinoForbiddenError
404FinoNotFoundError
409FinoConflictError
422FinoUnprocessableError
423FinoLockedError
429FinoRateLimitError (carries retryAfter and rateLimit)
5xxFinoServerError
—FinoNetworkError, FinoTimeoutError

Each one carries status, type, message, the raw body and the correlationId when the API sent one.

Escape hatch

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);

Staying current with the API

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.

End-to-end tests

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

What it covers

SuiteWhat it proves
01-platformAuth, token reuse, wrong-credential handling, /ready, category tree, webhooks list, company and logo search
02-usersCreate, read, update, resolve by customUserID, delete, and a 404 afterwards
03-person-analysisUpload → trigger → wait → categorization, contracts and every person scope
04-company-analysisThe same flow against every company scope
05-banking-upload-crudAccount and transaction writes, connect and management sessions, disconnect, optional demo bank login
06-webhooksRegister, 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.

Missing scopes are reported, not failed

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.

Safety rules

These tests are built to run against production, because that is where the test client lives.

  • They never initiate a money transfer. Not a default — createMoneyTransfer is on a blocklist that __tests__/e2e-guardrails.test.ts enforces on every commit.
  • They never write tenant-wide state: no advisor, tenant, logo or collect-account writes.
  • Everything they write lives inside a user they create and delete. Each user carries a 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.
  • Production needs a second switch. FINOOS_E2E_ALLOW_PRODUCTION=1 on top of FINOOS_E2E=1, so nobody reaches production by forgetting to set a base URL.
  • They never touch a webhook they did not create. finoOS allows one webhook per client, so a real tenant usually already has a live one. The suite snapshots the existing IDs first and every mutating call goes through a guard that refuses them.

Scripts

ScriptWhat it does
npm testnode --test, including the API drift gate and the e2e guard rails
npm run e2eThe end-to-end suite against a real environment, plus the coverage report
npm run typechecktsc --noEmit
npm run lint / lint:fixBiome
npm run buildESM bundle plus type declarations

License

MIT © Fino Digital GmbH

Keywords

fino

FAQs

Package last updated on 20 Aug 2026

Related posts