Finatic Client SDK
Browser SDK for embedding Finatic Connect and account-first data into your frontend. The company API key never belongs in this package. Your backend (Server SDK) mints a one-time token; the browser only receives that token.
Install
npm install @finatic/client
Quick start
import { FinaticConnect, isPortalLifecycleEventPayload } from '@finatic/client';
const finatic = await FinaticConnect.init('one-time-token', undefined, {
apiEnvironment: 'sandbox', // or 'live'
});
await finatic.openPortal({
mode: 'dark',
onSuccess: (userId) => {
// OTP / session ready. The user has not necessarily granted an account yet.
console.log('Portal user linked:', userId);
},
onEvent: async (eventName, payload) => {
if (eventName !== 'account.grant.created') return;
const grant = payload as { accountId?: string };
const accounts = await finatic.v1.listAccounts({ includeSyncStatus: true });
const ready = accounts.success?.data ?? [];
console.log('Grant created for', grant.accountId, 'accounts', ready);
},
});
openPortal must use the object form to receive onEvent. The positional overload has no grant events.
The embedding app controls when Connect is dismissed. Call closePortal()
from host UI or after handling an optional portal event; repeated calls are
safe and do nothing when no portal is open:
await finatic.openPortal({
onEvent: (eventName) => {
if (eventName === 'account.grant.created') {
finatic.closePortal();
}
},
onClose: () => {
// Runs once for either the portal close control or closePortal().
},
});
Portal authentication and connection readiness
onSuccess(userId) means the portal session authenticated. It does not mean a
broker connection exists or that connector data is ready. Connect schema v1
publishes those later transitions through the typed portal.lifecycle event:
await finatic.openPortal({
onEvent: (eventName, payload) => {
if (eventName !== 'portal.lifecycle' || !isPortalLifecycleEventPayload(payload)) return;
switch (payload.stage) {
case 'portal_authenticated':
console.log('Portal authenticated', payload.userId);
break;
case 'broker_connection_created':
console.log('Connection persisted', payload.connectionId);
break;
case 'push_agent_state_changed':
// Only LIVE_DATA and STALE_LIVE_DATA can report dataReady: true.
console.log(payload.state, payload.dataReady ?? 'not asserted');
break;
}
},
});
The SDK accepts lifecycle messages only when they match schema version 1 and
the exported stage/state allowlists. Unsupported versions, malformed payloads,
unexpected fields (including secret-bearing fields), and invalid readiness
claims are dropped. Existing broad (eventName: string, payload?: unknown)
handlers remain assignable, and non-lifecycle events continue to be forwarded.
Client data methods return the wire envelope { success, error, warning } (not { data, errors }).
Exact instrument descriptors
Account orders, order fills and events, positions, position lots, and lot fills expose the
API-generated FDXInstrumentDescriptor from the package root. Exact futures keep their full
contract identity (MGCZ6 and MGCG7 remain distinct); root-only symbols such as MGC are
marked with future.identityQuality === 'ROOT_ONLY'. Optional expiry, exchange, provider symbol,
and provenance fields stay absent when the API cannot verify them—the browser SDK does not infer
or normalize instrument identity.
import {
FDXFutureInstrumentDetailsIdentityQualityEnum,
type FDXBrokerPosition,
} from '@finatic/client';
const result = await finatic.v1.listPositions({ accountId: 'account-id' });
const position: FDXBrokerPosition | undefined = result.success?.data[0];
if (
position?.instrument?.future?.identityQuality ===
FDXFutureInstrumentDetailsIdentityQualityEnum.Exact
) {
console.log(position.instrument.finaticInstrumentId, position.instrument.future.contractCode);
}
await finatic.v1.createAccountOrder({
accountId: 'account-id',
idempotencyKey: crypto.randomUUID(),
body: {
order: {
symbol: 'MGCZ6',
finaticInstrumentId: 'fininst_mgcz6',
instrumentId: 'provider-native-id',
},
},
});
Legacy symbol-only order bodies and existing flat identity fields remain supported. When both identity fields are supplied, the API validates that they agree in the authorized account scope.
Event Contract capability and order types
The package root exports FDXAssetType.EventContract, EventContractCapabilityResponse,
AccountOrderSchemaResponse, and FinaticAPIErrorResponse from the approved public OpenAPI
artifact. Capability axes are independent: provider offering and implementation evidence do not
imply public write enablement or account entitlement. Treat publicEnabled !== true,
accountEntitled !== true, or closingOnly === true as a reason not to submit an opening order.
import {
FDXAssetType,
type AccountOrderSchemaResponse,
} from '@finatic/client';
const response = await finatic.v1.getAccountOrderSchema<AccountOrderSchemaResponse>({
accountId: 'account-id',
action: 'place',
});
const capability = response.success?.data.eventContract;
if (
capability?.publicEnabled === true &&
capability.accountEntitled === true &&
capability.closingOnly !== true
) {
await finatic.v1.createAccountOrder({
accountId: 'account-id',
idempotencyKey: crypto.randomUUID(),
body: { order: { assetType: FDXAssetType.EventContract, symbol: 'EVENT-SYMBOL' } },
});
}
EVENT_CONTRACT is the public asset enum value. Provider-native payload fields such as
asset_type: 'event_contract' pass through unchanged. Unknown public assetType values fail
locally instead of being omitted or mapped to equity. Authenticated Event Contract writes and
public enablement still require provider/account evidence; this SDK release does not claim them.
Call FinaticConnect.reset() before init when you need a new session boundary.
Token handoff
// Backend (@finatic/server-node) — keep FINATIC_API_KEY here
const token = await finatic.v1.getToken();
// 90 seconds. Send token to the browser only.
Never set an API key in Vite/NEXT_PUBLIC_ env vars for production.
Migrating from beta
| Beta route family | v1 replacement |
|---|---|
/api/beta/session/init | Backend v1.getToken() / session/init |
/api/beta/session/start | FinaticConnect.init(oneTimeToken) |
/api/beta/session/portal | finatic.openPortal(...) or backend v1.getPortalUrl() |
/api/beta/brokers/data/accounts | finatic.v1.listAccounts(...) |
/api/beta/brokers/data/balances | finatic.v1.listBalances({ accountId }) |
/api/beta/brokers/data/positions | finatic.v1.listPositions({ accountId }) |
/api/beta/brokers/data/orders | finatic.v1.listOrders({ accountId }) |
/api/beta/brokers/data/transactions | finatic.v1.listTransactions({ accountId }) |
/api/beta/brokers/orders | finatic.v1.createAccountOrder({ accountId, body, idempotencyKey }) |
/api/beta/brokers/connections | finatic.v1.listAccounts() plus finatic.v1.listAccountGrants() |
Connect owns portal institution/auth/consent UX. Those flows are not published methods on finatic.v1. Position lots are not on the v1 façade — use listPositions. Do not pass beta connectionId as {accountId}.
Environments
apiEnvironment is live | sandbox (X-Finatic-Environment). URL presets (environment: 'staging' | 'production' | ...) are separate from sandbox vs live data.
Common commands
| Task | Command |
|---|---|
| Build | npm run build |
| Test | npm test |
| Lint | npm run lint |
Documentation
This README is the Client SDK contract. Fetch the rest before writing a full integration:
- Quick start: https://finatic.dev/docs/quick-start/quick-start
- Node backend README: https://github.com/FinaticORG/FinaticServerSDK-Node/blob/develop/README.md
- Python backend README: https://github.com/FinaticORG/FinaticServerSDK-Python/blob/develop/README.md
- Embed Connect: https://finatic.dev/docs/quick-start/portal-configuration
- Demo apps: https://github.com/FinaticORG/FinaticDemoApps/blob/develop/README.md
- API reference: https://finatic.dev/docs/api-reference
- OpenAPI: https://finatic.dev/openapi.json
- Agent index: https://finatic.dev/llms.txt
- Agent notes: https://finatic.dev/AGENTS.md