TypeScript SDK
@fathom-charts/sdk handles authentication, the initial snapshot, resuming after a disconnect, duplicates, history pagination, exports and rate-limit retries for you. This page describes every option and method.
Install
npm install @fathom-charts/sdk ws- ESM only: the package ships no CommonJS build. Your project must declare
"type": "module"in itspackage.json(or use.mjs/.mtsfiles). - Node 22 or later, and browsers.
exportNdjson()requires Node 22.15 or later (zstd decompression fromnode:zlib). wsis only needed in Node: it opens the connection with theAuthorizationheader. In a browser, the SDK uses the nativeWebSocketwith a stream token, and never importsws.
The SDK imports ws dynamically, with the module name in a variable, so that browser bundlers leave it out. Webpack then reports Critical dependency: the request of a dependency is an expression: this warning is harmless.
Indicator types
The SDK is generic: an indicator’s data is typed by the types you generate from the catalog (GET /v1/indicators, no authentication).
curl https://api.fathomcharts.com/v1/indicators -o catalog.json
npx fathom-charts-types catalog.json src/fathom-charts-types.tsThe generated file exports, for each indicator, <Name>Params and <Name>Data (BigTradesData, FootprintData…), plus IndicatorParams, IndicatorData and IndicatorId. The catalog descriptions become JSDoc comments; a footprint level is typed [number, number, number, number?] (the 4th element is optional). The output folder is created if it does not exist. Generate the file again when the catalog changes (its engineFingerprint changes with the computation).
Real time: FathomChartsStream
import { FathomChartsStream } from '@fathom-charts/sdk';
import type { BigTradesData } from './fathom-charts-types.js';
const stream = new FathomChartsStream({
url: 'wss://stream.fathomcharts.com/v1',
apiKey: process.env.FATHOM_CHARTS_API_KEY!,
onStateChange: (state) => console.log('connection', state),
});
const sub = stream.subscribe<BigTradesData>({
instrument: 'NQ.front',
indicator: 'big-trades',
params: { minimum: 30 },
mode: 'confirmed',
});
for await (const event of sub) {
if (event.type === 'upsert') console.log(event.cursor, event.data.side, event.data.volume);
}Options
| Option | Purpose |
|---|---|
url | Required. The stream address: wss://stream.fathomcharts.com/v1. |
apiKey | API key, sent in the Authorization: Bearer header. Server-side only; requires the ws package unless you provide webSocket. |
streamToken | In a browser, instead of apiKey: an async () => string function that asks your server for a stream token. It is called on every connection, reconnections included. Give exactly one of apiKey and streamToken, otherwise the constructor throws CONFIGURATION. |
webSocket | Optional: a (url, headers) => WebSocket function that opens the connection, to use another implementation than ws or the browser’s WebSocket. headers holds the Authorization header with apiKey, undefined with streamToken. |
reconnect | Optional: initialDelayMs (first reconnection delay, 250 ms by default), maxDelayMs (backoff ceiling, 30 s), quotaRetryMs (how long a 4029 close code is retried, 120 s), quotaDelayMs (minimum delay between two attempts after a 4029, 5 s). |
heartbeatTimeoutMs | Silence after which the connection is considered lost, then reopened. 15,000 ms by default: the server sends an hb every 5 seconds. |
highWaterMark | Number of events waiting to be read, all subscriptions together, above which the SDK slows down reading the socket (with ws) and keeps only the latest version of each object in progress. 10,000 by default. |
maxBuffered | When the socket cannot be paused (browsers): number of waiting events above which the SDK closes the connection, then reopens it from the last cursor received once half of highWaterMark has been read. Nothing is lost. 4 × highWaterMark by default. |
onStateChange | Called on every change of connectionState. |
onNotice | Called with each notice message from the server ({ kind, effectiveAt? }), for example a maintenance announcement (kind: "reconnect"). The SDK reconnects on its own. |
subscribe()
stream.subscribe<D>(options) opens a subscription and returns a Subscription object, to read with for await. Its options are those of the subscribe message: instrument, indicator, params ({} by default), mode (live by default) and from ("live" by default, { cursor } or { time }). The SDK picks the sub name itself. The type D is the indicator’s data type (BigTradesData…).
The loop receives events whose type field is the name of the server message, without the sub field:
type | Fields | When |
|---|---|---|
subscribed | topic, instrument, tickSizeNanos | The subscription is accepted. Arrives again after every reconnection, and after a reset with reason roll or entitlement_changed. |
snapshot | cursor, items (each { id, final, ts, data }) | The starting state. Replace your whole local state with items. |
upsert | cursor, id, final, ts, data | Object id is created or entirely replaced. |
remove | cursor, id, final, ts | Object id is deleted. |
status | state, lagMs | The stream status (see Stream status). Arrives again after every reconnection. |
reset | reason | Your local state is no longer valid: discard it, a snapshot follows. |
The other server messages do not become events: the SDK handles hb (silent connection detection, resume cursor), pong and notice (passed to onNotice) itself. An error on the subscription ends the loop: for await throws a FathomChartsError carrying the received code (INVALID_PARAMETERS, UNKNOWN_INSTRUMENT, FORBIDDEN, QUOTA_EXCEEDED, NOT_COVERED…).
The SDK drops duplicates: after a resume, no upsert or remove whose cursor is not strictly greater than the last one received is handed to you.
Subscription
| Member | Purpose |
|---|---|
cursor | The cursor of the last event handed to you: your resume point. undefined until a cursor has arrived. |
topic | The topic of the latest subscribed. |
close() | Stops the subscription: the SDK sends unsubscribe, and a pending loop ends. |
Leaving the loop (break, return or an exception in its body) calls close().
Connection lifecycle
All the subscriptions of a FathomChartsStream share one connection, which counts against the connections of your plan:
- the connection opens on the first
subscribe(); - when the last subscription closes (
break,close(), or a servererroron the last subscription), the SDK closes the connection (code1000, reasonidle) andconnectionStatebecomesidle. A newsubscribe()opens it again; stream.close()closes every subscription and the connection. Calling it right aftersubscribe(), before the connection is open, is safe. Asubscribe()afterclose()throwsCLOSED;connectionStateisidle,connecting,open,reconnectingorclosed.
After a disconnect, the SDK reconnects with exponential backoff and jitter, and sends each subscribe again with from: { cursor }, the last cursor received: you receive subscribed and status again, then the missed messages, with no snapshot when the resume succeeds, or a reset followed by a snapshot otherwise. A from: { time } subscription that already received a cursor resumes from that cursor: the past range is never replayed a second time. Only a disconnect during warming, before the first cursor, starts it again from the original time, and the part replayed from history is then charged again.
| Close code | What the SDK does |
|---|---|
4001 | Ends every subscription with UNAUTHORIZED, without reconnecting. |
4003 | Ends every subscription with KEY_REVOKED, ENTITLEMENTS_CHANGED or FORBIDDEN, depending on the close reason, without reconnecting. |
1002, 1003, 1007, 1009 | Ends every subscription with PROTOCOL_ERROR, BINARY_NOT_SUPPORTED, INVALID_UTF8 or MESSAGE_TOO_BIG: the same messages would be refused again. |
4029 | Retries for reconnect.quotaRetryMs (a connection lost without a close expires on the server after 90 seconds), then ends everything with TOO_MANY_CONNECTIONS. |
4008 | Reconnects right away and resumes from the last cursor. |
Others, or heartbeatTimeoutMs without a message | Reconnects with backoff (at least 1 second after a 1013). |
After one of these final errors, a new subscribe() on the same FathomChartsStream throws the same error: fix the cause, then create a new FathomChartsStream.
Resume after a restart
The SDK resumes on its own after a disconnect, as long as your process runs. To resume after your process restarts, save sub.cursor as you go (along with the state you built from it), then open the subscription again with from: { cursor }:
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { FathomChartsStream } from '@fathom-charts/sdk';
import type { BigTradesData } from './fathom-charts-types.js';
// A file here; in production, the storage of your state (a database…).
const CURSOR_FILE = 'big-trades.cursor';
const saved = existsSync(CURSOR_FILE) ? readFileSync(CURSOR_FILE, 'utf8') : undefined;
const stream = new FathomChartsStream({
url: 'wss://stream.fathomcharts.com/v1',
apiKey: process.env.FATHOM_CHARTS_API_KEY!,
});
const sub = stream.subscribe<BigTradesData>({
instrument: 'NQ.front',
indicator: 'big-trades',
params: { minimum: 30 },
mode: 'confirmed',
from: saved ? { cursor: saved } : 'live',
});
for await (const event of sub) {
if (event.type === 'upsert') console.log(event.data.side, event.data.volume);
// The cursor is saved once the event is handled.
if (sub.cursor) writeFileSync(CURSOR_FILE, sub.cursor);
}If the cursor is too old, you receive a reset then a new snapshot: start over from it.
History: FathomChartsRest
import { FathomChartsRest } from '@fathom-charts/sdk';
const rest = new FathomChartsRest({ baseUrl: 'https://api.fathomcharts.com', apiKey: process.env.FATHOM_CHARTS_API_KEY! });| Option | Purpose |
|---|---|
baseUrl | Required. The API address: https://api.fathomcharts.com. |
apiKey | Required. API key, sent in the Authorization: Bearer header. Server-side only. |
fetch | Optional: a fetch implementation to use instead of the platform’s. |
rateLimitRetries | Number of retries after a 429 RATE_LIMITED. 3 by default. |
Methods
Every method that reads history takes the query described on the History page (instrument, indicator, params, from, to, mode, and depending on the method limit, cursor, untilCursor, snapshot), and an optional AbortSignal.
| Method | Returns |
|---|---|
page(query, signal?) | One page: { instrument, tickSizeNanos, snapshot?, items, next, computeUnits }. |
pages(query, signal?) | The pages one at a time, following next, as you read them. |
history(query, signal?) | The results one at a time, across pages. With snapshot: true, the first element is the state at from, { t: 'snapshot', cursor, items }, then come the updates (t is upsert or remove, each with its cursor). |
estimate(query, signal?) | The cost estimate, charging nothing. Takes the same query as page(). |
export(query, signal?) | One export response: { body, contentType, filename, token }. body is the zstd-compressed file as is; token resumes the export with resume. |
exportNdjson(query, options?) | The decompressed export, in chunks (Uint8Array) of whole lines, with automatic resume. See Exports. |
streamToken() | A browser stream token: { token, expiresAt }. |
usage() | Your usage this month and your limits. |
instruments() | { instruments }: every instrument served, with symbol, alias, tickSizeNanos, status and updatedAt. |
status() | { engine: { state }, instruments }: the state of the computation (up or down) and of each instrument. |
offers() | The offers and the limits of each combination (GET /v1/offers). |
indicators() | The indicator catalog: { version, engineFingerprint, indicators }. |
The options of exportNdjson(): signal, maxResumes (consecutive resumes without a new line before giving up, 5 by default) and resumeDelayMs (delay before the first resume, 1 s by default, doubled on each consecutive resume up to 30 s). If the very first request fails (network error or 5xx before the X-Export-Token header), exportNdjson() never sends it again: a new export would be charged again. It throws the error, and you decide.
Rate limit and retries
A 429 RATE_LIMITED is retried up to rateLimitRetries times: after the delay of the Retry-After header (60 seconds at most), or, without that header, after 1, 2, then 4 seconds. An AbortSignal ends the wait right away. 429 QUOTA_EXCEEDED (monthly budget used up) is never retried: it is thrown at once.
Errors
Every SDK error is a FathomChartsError:
| Field | Content |
|---|---|
code | The stable error code: an API code (see Errors), an SDK code (below), or HTTP_<status> (HTTP_502, for example) for an HTTP error response without a problem+json document. |
message | A human-readable explanation. Over REST: the detail of the response, followed by each invalid field (<detail>: <path> <message>; …). |
details.status | The HTTP status, over REST. |
details.closeCode | The WebSocket close code, when the connection was closed. |
details.retryAfter | The value of the Retry-After header, in seconds, when the response has one. |
details.problem | The full problem+json document of the response, with errors when the API lists invalid fields. |
SDK codes, in addition to the API codes (the full list is exported as ERROR_CODES):
code | Cause |
|---|---|
CONFIGURATION | Invalid options (apiKey and streamToken together, or neither), or a feature missing from the platform: the ws package for an API key, any WebSocket implementation, zstd decompression for exportNdjson(). |
CLOSED | subscribe() on a FathomChartsStream closed by close(). |
KEY_REVOKED | The connection was closed (4003) because the key was revoked or expired. |
ENTITLEMENTS_CHANGED | The connection was closed (4003) because your offers no longer cover it (offer ended, account suspended, subscription quota exceeded). |
TOO_MANY_CONNECTIONS | The maximum number of connections of your plan stayed reached (4029) for reconnect.quotaRetryMs. |
PROTOCOL_ERROR, BINARY_NOT_SUPPORTED, INVALID_UTF8, MESSAGE_TOO_BIG | The server refused a message sent by the client (close codes 1002, 1003, 1007, 1009). |
HTTP_<status> | An HTTP error response without a problem+json document. |
A 4001 close gives UNAUTHORIZED, with a message listing the possible causes (key missing, malformed, unknown, revoked or expired, stream token invalid, expired or already used).
Cursors
compareCursors(a, b) compares two cursors <utcDay>.<rank>.<k> number by number and returns a negative number, zero or a positive number. A malformed cursor (numbers with leading zeros, a missing field…) throws INVALID_PARAMETERS.
import { compareCursors } from '@fathom-charts/sdk';
compareCursors('20720.111416.0', '20720.111417.0'); // < 0