# 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

```bash
npm install @fathom-charts/sdk ws
```

- **ESM only**: the package ships no CommonJS build. Your project must declare `"type": "module"` in its `package.json` (or use `.mjs` / `.mts` files).
- **Node 22 or later**, and browsers. `exportNdjson()` requires Node 22.15 or later (zstd decompression from `node:zlib`).
- **`ws`** is only needed in Node: it opens the connection with the `Authorization` header. In a browser, the SDK uses the native `WebSocket` with a [stream token](https://fathomcharts.com/en/docs/authentication.md#browser-access), and never imports `ws`.

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

```bash
curl https://api.fathomcharts.com/v1/indicators -o catalog.json
npx fathom-charts-types catalog.json src/fathom-charts-types.ts
```

The 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

```ts
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`](https://fathomcharts.com/en/docs/websocket.md#subscribing) 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](https://fathomcharts.com/en/docs/websocket.md#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 server `error` on the last subscription), the SDK closes the connection (code `1000`, reason `idle`) and `connectionState` becomes `idle`. A new `subscribe()` opens it again;
- `stream.close()` closes every subscription and the connection. Calling it right after `subscribe()`, before the connection is open, is safe. A `subscribe()` after `close()` throws `CLOSED`;
- `connectionState` is `idle`, `connecting`, `open`, `reconnecting` or `closed`.

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](https://fathomcharts.com/en/docs/websocket.md#starting-in-the-past) 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 }`:

```ts
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

```ts
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](https://fathomcharts.com/en/docs/history.md#make-a-query) 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](https://fathomcharts.com/en/docs/history.md#estimate-the-cost), charging nothing. Takes the same query as `page()`.                                                                                    |
| `export(query, signal?)`        | One [export](https://fathomcharts.com/en/docs/exports.md) 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](https://fathomcharts.com/en/docs/exports.md#with-the-sdk).                                                          |
| `streamToken()`                 | A browser stream token: `{ token, expiresAt }`.                                                                                                                                                                            |
| `usage()`                       | Your [usage](https://fathomcharts.com/en/docs/limits.md#track-your-usage) this month and your limits.                                                                                                                      |
| `instruments()`                 | `{ instruments }`: every [instrument](https://fathomcharts.com/en/docs.md#instruments) 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](https://fathomcharts.com/en/docs/errors.md#code-reference)), 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`.

```ts
import { compareCursors } from '@fathom-charts/sdk';

compareCursors('20720.111416.0', '20720.111417.0'); // < 0
```
