# Fathom Charts documentation > Footprint, big trades, imbalances, VWAP… 10 order flow indicators on futures, computed server-side with your parameters. Real time over WebSocket, historical over REST. Source: https://fathomcharts.com/en/docs # Build with Fathom. Fathom Charts computes your order flow indicators server-side and sends you the results, ready to display. In real time over WebSocket, or over any past period through the REST API, with exactly the same parameters. - [Quickstart](https://fathomcharts.com/en/docs/quickstart.md): From API key to first message in a few minutes. - [Authentication](https://fathomcharts.com/en/docs/authentication.md): API keys and browser access. - [Real time](https://fathomcharts.com/en/docs/websocket.md): Subscribe, keep your state in sync, resume after a disconnect. - [History](https://fathomcharts.com/en/docs/history.md): Compute an indicator over a past period. - [Exports](https://fathomcharts.com/en/docs/exports.md): Download a long period as a single file. - [Indicators](https://fathomcharts.com/en/docs/indicators.md): Parameters and data for every indicator. ## How it works You pick three things: - an **instrument**: a specific futures contract such as `NQZ6`, or `NQ.front` to follow the active contract automatically; - an **indicator** from the [catalog](https://fathomcharts.com/en/docs/indicators.md): `big-trades`, `footprint`, `liquidity-walls`…; - its **parameters**: any you leave out take their default value. The server runs the indicator on every trade in the market and sends you **objects**: a large order (Fathom Trades), a footprint bar, a zone… There is nothing to recompute on your side: display them or store them. ## Objects Every object has a stable `id`. The server sends two kinds of updates: - `upsert`: creates the object, or **replaces it entirely** if it already exists; - `remove`: deletes the object. An object still being built (the current minute's footprint bar, for example) arrives with `final: false` and can still change. Once complete, it is sent one last time with `final: true`. Only care about completed results? Subscribe in `confirmed` mode. Prices are expressed in **ticks**. To get a price in points, multiply by the instrument's tick size, given in billionths by the `tickSizeNanos` field (see [Instruments](#instruments)): `"250000000"` for NQ, that is 0.25, so 122017 ticks × 0.25 = 30,504.25. A trade price is always a whole number of ticks, but a computed price can fall between two ticks: a VWAP, the middle of a range (the `MP` level of `key-levels`, (high + low) / 2), or the bounds of an `effort-zones` zone trimmed by half. Treat prices as numbers, not integers. Times (`ts`, `start`, `end`…) are timestamps in nanoseconds since January 1, 1970 UTC, as strings. Some fields count bars rather than time: in `effort-zones`, `bar.start` is a timestamp (string), while `zone.start` and `zone.end` are bar indices (numbers). Each [indicator](https://fathomcharts.com/en/docs/indicators.md) page gives the unit of every field. ## Instruments `GET /v1/instruments` lists the instruments served and their tick size. It requires an API key, whatever its scope. ```bash curl https://api.fathomcharts.com/v1/instruments -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" ``` ```json { "instruments": [ { "dataset": "GLBX.MDP3", "symbol": "NQZ6", "alias": "NQ.front", "instrumentId": "42140878", "tickSizeNanos": "250000000", "status": "live", "updatedAt": "1790256600000000000" } ] } ``` | Field | Content | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dataset` | The data source of the contract (`GLBX.MDP3`). | | `symbol` | The contract symbol, to pass as `instrument`. | | `alias` | The alias that designates this contract today (`NQ.front`), or `null`. | | `instrumentId` | The numeric id of the contract at our data provider, as a string, or `null`. | | `tickSizeNanos` | The tick size in billionths (units of 10⁻⁹), as a string: `"250000000"` = 0.25. | | `status` | The state of the instrument’s data: `live`, `warming`, `source_delayed`, `source_down` or `market_closed` (see [Stream status](https://fathomcharts.com/en/docs/websocket.md#stream-status)). | | `updatedAt` | When this information last changed: a timestamp in nanoseconds, as a string. | Symbols and aliases are case-sensitive. ## Sessions, weeks and front month - **Sessions**: a session runs from 18:00 to 17:00 the next day, New York time, and is named by its closing date: session `2026-09-24` starts on September 23 at 18:00. The market pauses every day from 17:00 to 18:00, and over the weekend from Friday 17:00 to Sunday 18:00, New York time. - **Weeks**: the weekly indicators (`effort-zones`, `liquidity-walls`, `flow-tracker`, `imbalances`, `vwap-bands`, `session-profile`, `opening-range`) start over on Saturday at 00:00 UTC, during the weekend closure. Their `id`s that hold a date carry that Saturday’s (`bar:2026-09-19:2173`). `key-levels` names a week by its Sunday (`week:2026-09-20`). - **Opening**: `opening-range` measures the range from 09:30 New York time, daylight saving changes included. - **Front month**: `NQ.front` designates the most traded NQ contract (ranked by volume), re-evaluated every day. When it changes, every subscription on `NQ.front` receives a `reset` with reason `roll`, then carries on with the new contract (see [Reset](https://fathomcharts.com/en/docs/websocket.md#reset)). Over history, an alias reads the contract it designated at `from`, and the response names it in `instrument`. ## Cursors Every update carries a `cursor`: its position in the stream. Keep the last one you received. After a disconnect, it lets you resume exactly where you left off, with nothing missed and nothing duplicated. See [Cursors and resuming](https://fathomcharts.com/en/docs/websocket.md#cursors-and-resuming), and their [format](https://fathomcharts.com/en/docs/history.md#cursors). ## Real time and history, same results The same computation runs live and over history. A historical query returns exactly the objects, values and cursors the real-time stream published. Backtest a strategy on the past, then plug it into the live market with no discrepancy. The data comes from the market's trades, provided by Databento. If the source is interrupted, the stream tells you: no data is ever made up or filled in. ## REST API reference The OpenAPI 3.1 description of the REST API is served at [`https://api.fathomcharts.com/docs/json`](https://api.fathomcharts.com/docs/json), with a Swagger UI at [`https://api.fathomcharts.com/docs`](https://api.fathomcharts.com/docs). It gives the schema of every endpoint, request and response. ## With your AI assistant Every page is also available as Markdown for Claude, ChatGPT or your coding agent, indexed in [`/llms.txt`](https://fathomcharts.com/llms.txt). See [Use with an LLM](https://fathomcharts.com/en/docs/llm.md). ## Try it for free The Sandbox is free: a stream delayed by 10 minutes and 7 days of history, enough to build your whole integration. The Live offer adds real-time data, the Historical offer lifetime access to the full history; you can combine both. See [Offers and billing](https://fathomcharts.com/en/docs/billing.md), then get going with the [quickstart](https://fathomcharts.com/en/docs/quickstart.md). --- Source: https://fathomcharts.com/en/docs/quickstart # Quickstart In five minutes: create a key, open a connection, subscribe to large aggressive orders on the Nasdaq (NQ), then ask for the same thing over a past period. ## 1. Create an API key [Sign in](https://fathomcharts.com/en/login) with your email address: you get a sign-in link, no password needed, and your account is created the first time you sign in. Then create a key in your [account](https://fathomcharts.com/en/account). **Copy it right away**: it is shown only once. There are two kinds of keys: - **test key** (`fc_test_…`): free, data delayed by 10 minutes. Ideal for development; - **live key** (`fc_live_…`): gets the rights of your offers, including real-time data with the [Live](https://fathomcharts.com/en/docs/billing.md) offer. Without an offer, it has the same rights as a test key. Store the key in an environment variable on your server: ```bash export FATHOM_CHARTS_API_KEY="fc_test_…" ``` Never put it in code that runs in a browser. For a web page, see [Browser access](https://fathomcharts.com/en/docs/authentication.md#browser-access). ## 2. Open a connection For a first try, the [wscat](https://github.com/websockets/wscat) command-line tool is all you need: ```bash npx wscat -c wss://stream.fathomcharts.com/v1 -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" ``` ## 3. Subscribe Once connected, send this message. It asks for large aggressive orders of at least 30 contracts ([Fathom Trades](https://fathomcharts.com/en/docs/indicators/big-trades.md)) on the active NQ contract: ```json {"t":"subscribe","sub":"bt","instrument":"NQ.front","indicator":"big-trades","params":{"minimum":30},"mode":"confirmed","from":"live"} ``` - `sub` is a name you choose for this subscription. Every message about it carries that name, so you can open several subscriptions on the same connection; - `mode: "confirmed"` sends only completed orders. With `mode: "live"`, you also get the order in progress, updated on every trade. ## 4. Read the messages Here is what this subscription received at the US open on September 24, 2026, with a live key and the Live offer: ```json {"sub":"bt","t":"subscribed","topic":"b2af53ed8e6cfc8620b4754c0836e0e2","instrument":"NQZ6","tickSizeNanos":"250000000"} {"sub":"bt","t":"snapshot","cursor":"20720.111120.0","items":[]} {"sub":"bt","t":"status","state":"live","lagMs":4} {"sub":"bt","cursor":"20720.111434.0","t":"upsert","id":"1790256600533489285:0","final":true,"ts":"1790256600535054015","data":{"side":"sell","volume":66,"trades":32,"start":"1790256600533489285","end":"1790256600533489285","first":122041,"last":122017,"price":122017,"vwap":122027.71212121213}} {"sub":"bt","cursor":"20720.112164.0","t":"upsert","id":"1790256605021635921:0","final":true,"ts":"1790256605033449615","data":{"side":"buy","volume":32,"trades":30,"start":"1790256605021635921","end":"1790256605023486337","first":122101,"last":122114,"price":122114,"vwap":122110.125}} {"t":"hb","cursors":{"bt":"20720.112164.0"}} ``` In order: 1. `subscribed`: the subscription is accepted, on the `NQZ6` contract that `NQ.front` designates; 2. `snapshot`: the objects that already existed when you subscribed (none here); 3. `status`: the stream is live; 4. `upsert`: a new object, here a completed large order; 5. `hb`: the heartbeat, sent every 5 seconds to confirm the connection is alive. The first `upsert` describes a **sell** order of **66 contracts**, filled in 32 trades, that pushed the price down from 122041 to 122017 ticks, or from 30,510.25 to 30,504.25 points (one tick is 0.25 points on NQ). Each instrument’s tick size is given by `GET /v1/instruments`, in billionths, in the `tickSizeNanos` field (`"250000000"` for NQ): see [Instruments](https://fathomcharts.com/en/docs.md#instruments). Every field is documented on the [indicator](https://fathomcharts.com/en/docs/indicators/big-trades.md) page. With a test key, you get the same messages 10 minutes late. While the market is closed (weekend, daily break), the `status` is `market_closed` and no `upsert` arrives before it reopens. Keep the `cursor` of the last message you received: you will need it to [resume](https://fathomcharts.com/en/docs/websocket.md#cursors-and-resuming) after a disconnect. ## 5. Use the SDK The `@fathom-charts/sdk` TypeScript SDK (Node 22+ and browsers) handles authentication, the initial snapshot, resuming after a disconnect and duplicates for you. In Node, also install the `ws` package: ```bash npm install @fathom-charts/sdk ws ``` Then generate the types of each indicator's data from the catalog (`GET /v1/indicators`, no authentication): ```bash curl https://api.fathomcharts.com/v1/indicators -o catalog.json node node_modules/@fathom-charts/sdk/scripts/generate-types.mjs catalog.json src/fathom-charts-types.ts ``` Then, in `src/`: ```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!, }); const bigTrades = stream.subscribe({ instrument: 'NQ.front', indicator: 'big-trades', params: { minimum: 30 }, mode: 'confirmed', }); for await (const event of bigTrades) { if (event.type === 'upsert') { console.log(event.data.side, event.data.volume, event.data.price); } } ``` Each server message becomes an event whose `type` field is the message name (`upsert`, `remove`, `snapshot`, `status`…). ## 6. Run the same request over history The same `instrument`, `indicator` and `params` work over a past period. Times are timestamps in nanoseconds since January 1, 1970 (UTC), sent as strings. Here, the last 3 completed days: REST history stops at midnight UTC (the current day is not in it yet), and the range stays within the Sandbox's 7 days, so it works with a test key. ```bash TODAY=$(( $(date +%s) / 86400 * 86400 )) FROM="$((TODAY - 3 * 86400))000000000" TO="${TODAY}000000000" curl https://api.fathomcharts.com/v1/indicator-queries \ -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"instrument\":\"NQ.front\",\"indicator\":\"big-trades\",\"params\":{\"minimum\":30},\"from\":\"$FROM\",\"to\":\"$TO\"}" ``` You get a page of results (`items`) and a `next` field: when it is not `null`, send the same request with `"cursor": ""` to get the next page. Each object has the same `id` and the same `cursor` as when the real-time stream published it. History also includes the intermediate versions (`final: false`) published while each object was being built: keep only `final: true` if you only care about completed results. See [History](https://fathomcharts.com/en/docs/history.md). ## Next steps - [Authentication](https://fathomcharts.com/en/docs/authentication.md): restrict a key, rotate it, open the stream from a browser. - [Real time](https://fathomcharts.com/en/docs/websocket.md): keep your state in sync, resume after a disconnect. - [Indicator catalog](https://fathomcharts.com/en/docs/indicators.md): parameters and data for every indicator. - [Use with an LLM](https://fathomcharts.com/en/docs/llm.md): give this documentation to your AI assistant or coding agent. --- Source: https://fathomcharts.com/en/docs/authentication # Authentication Your server authenticates with an API key. A web page never holds a key: your server hands it a short-lived token to open the stream. ## API keys Create your keys in your [account](https://fathomcharts.com/en/account). A key is shown **only once**, when it is created: copy it right away. We keep no readable copy, so a lost key cannot be recovered, only [rotated](#rotating-a-key). | Key | Data | Use | | ----------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | | `fc_test_…` | Sandbox rights: delayed by 10 minutes, 7 days of history | Building and testing your integration, for free | | `fc_live_…` | The rights of your offers: real time with Live, full history with Historical. Without an offer, the Sandbox rights | Production | A test key always has the Sandbox rights, whatever your offer. Apart from the delay, it behaves exactly like a live key: same messages, same cursors, same responses. Send the key in the `Authorization` header, on every REST request and when opening the WebSocket: ```bash curl https://api.fathomcharts.com/v1/usage -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" ``` The word `Bearer` is case-insensitive, but required: a bare key without `Bearer` is refused (`401` over REST, code `4001` on the WebSocket). A missing, invalid, expired or revoked key is rejected with `401 UNAUTHORIZED` over REST, with the header `WWW-Authenticate: Bearer realm="fathomcharts"` (plus `error="invalid_token"` when a key was sent). On the WebSocket, the connection opens first (a browser cannot read why an opening was refused), then it is closed right away with code `4001`. A valid key that lacks the required scope is rejected with `403 FORBIDDEN`, or code `4003` on the WebSocket. ## Scopes and restrictions A key only grants what you allow it to. If it leaks, the damage stays contained: a key that only reads the stream has no business starting exports. | Scope (`scopes`) | Allows | | ---------------- | ------------------------------------------------------------------------------------- | | `stream` | The real-time stream, and creating [browser tokens](#browser-access) | | `history` | [Historical queries](https://fathomcharts.com/en/docs/history.md) and their estimates | | `export` | [Exports](https://fathomcharts.com/en/docs/exports.md) | | `keys` | Managing keys through the API | You can also restrict a key to specific instruments or IP addresses, and give it an expiry date. Through the API, with a key that has the `keys` scope: ```bash curl https://api.fathomcharts.com/v1/keys \ -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"collecteur","environment":"live","scopes":["stream","history"],"instruments":["NQ.front"],"allowedIps":["203.0.113.0/24"]}' ``` | Field | Content | | ------------- | -------------------------------------------------------------------------------- | | `name` | Key name, 1 to 64 characters | | `environment` | `live` or `test` | | `scopes` | At least one of `stream`, `history`, `export`, `keys` | | `instruments` | Optional: allowed instruments (up to 32). Omitted: all | | `allowedIps` | Optional: allowed IP addresses or CIDR ranges (up to 32). Omitted: any | | `expiresAt` | Optional: expiry date, as a timestamp in nanoseconds since January 1, 1970 (UTC) | The response contains the full key in the `key` field, once. A key cannot create a key with broader rights than its own, nor a key of the other kind (test or live). `GET /v1/keys` lists your keys. An account can have up to **10 active keys**. All your keys share the same [limits](https://fathomcharts.com/en/docs/limits.md): creating more does not raise your quotas. ## Rotating a key To change a key without interrupting your service, rotate it (``: the key's `id` field, from `GET /v1/keys`): ```bash curl -X POST https://api.fathomcharts.com/v1/keys//rotate \ -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"gracePeriodSeconds":172800}' ``` You get a new key with the same scopes and restrictions. The old one keeps working during the grace period (`gracePeriodSeconds`, up to 7 days; 2 days here), giving you time to deploy the new key everywhere, and is then revoked. With `0`, it is revoked immediately. A key can only be rotated once: a second rotation is rejected with `409 CONFLICT`. ## Revoking a key `DELETE /v1/keys/`, or from your [account](https://fathomcharts.com/en/account). Revocation is immediate: WebSocket connections opened with the key are closed within a second (code `4003`, reason `KEY_REVOKED`). ## Browser access An API key must never appear in a web page: any visitor could read it. To show the stream in a browser: 1. your server requests a **stream token** with its key; 2. it passes the token to the page; 3. the page opens the WebSocket and sends the token. On the server: ```bash curl -X POST https://api.fathomcharts.com/v1/stream-tokens -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" ``` The response contains the token (`token`) and its expiry (`expiresAt`). With the SDK: `FathomChartsRest.streamToken()`. In the page, give the SDK a function that fetches a token from your server (here a `/api/fathom-charts-token` route of your backend that makes the call above). The SDK calls it on every connection and handles the rest: ```ts import { FathomChartsStream } from '@fathom-charts/sdk'; const stream = new FathomChartsStream({ url: 'wss://stream.fathomcharts.com/v1', streamToken: async () => { const response = await fetch('/api/fathom-charts-token', { method: 'POST' }); const { token } = (await response.json()) as { token: string }; return token; }, }); for await (const event of stream.subscribe({ instrument: 'NQ.front', indicator: 'big-trades', params: { minimum: 30 } })) { console.log(event.type, event); } ``` Without the SDK, send the token in an `auth` message, first, within 5 seconds of opening the connection: ```ts const { token } = (await fetch('/api/fathom-charts-token', { method: 'POST' }).then((r) => r.json())) as { token: string }; const ws = new WebSocket('wss://stream.fathomcharts.com/v1'); ws.onopen = () => { ws.send(JSON.stringify({ t: 'auth', token })); ws.send(JSON.stringify({ t: 'subscribe', sub: 'bt', instrument: 'NQ.front', indicator: 'big-trades', params: { minimum: 30 }, mode: 'live', from: 'live', })); }; ws.onmessage = (e) => console.log(JSON.parse(e.data)); ``` Good to know: - a token is valid for **60 seconds** and works **only once**. It opens the connection, which then stays open as long as you need. Every reconnection needs a new token; - the token has the same scopes and limits as the key that created it, and stops working if that key is revoked; - any website can use a token: only hand tokens to your own users, once they are authenticated. If the first message is not a valid `auth`, or does not arrive within 5 seconds, the connection is closed with code `4001`. ## Signing in to the portal Your [account](https://fathomcharts.com/en/account) (keys, offers, billing, usage) has no password. On the [Sign in](https://fathomcharts.com/en/login) page, enter your email address: you receive a sign-in link **valid for 15 minutes, usable once**. You can request up to 5 links per hour. Link rejected? It has expired or was already used: request a new one. Once signed in, your session stays open for 30 days in that browser. --- Source: https://fathomcharts.com/en/docs/llm # Use with an LLM The whole documentation is available as Markdown, the format AI assistants and coding agents (Claude, ChatGPT, Claude Code, Cursor, Codex…) read best. Point them at it: they write your integration from the current API, not from memory. ## On every page At the top of each documentation page: - **Copy page** copies the page as Markdown, ready to paste into a conversation; - **Markdown** opens the Markdown version of the page; - **Open in Claude** and **Open in ChatGPT** start a conversation that reads the page, so you can ask your questions right away. ## llms.txt Two files at the root of the site follow the [llms.txt](https://llmstxt.org) convention: | File | Content | When to use it | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | [`/llms.txt`](https://fathomcharts.com/llms.txt) | Index: every page with a one-line summary, linking to its Markdown version, plus the machine-readable API references | Let the agent pick the pages it needs | | [`/llms-full.txt`](https://fathomcharts.com/llms-full.txt) | The whole English documentation in a single file, indicator pages included | Load everything into the context at once | ## Markdown pages Append `.md` to the address of any documentation page to get its Markdown version: ```bash curl https://fathomcharts.com/en/docs/quickstart.md ``` Agents that send `Accept: text/markdown` (Claude Code, Cursor…) get the Markdown directly at the usual address: ```bash curl -H "Accept: text/markdown" https://fathomcharts.com/en/docs/quickstart ``` French pages work the same way: [`/docs/demarrage-rapide.md`](https://fathomcharts.com/docs/demarrage-rapide.md.md). Links between Markdown pages point to Markdown pages, so an agent can follow them. ## Machine-readable references Beyond the documentation, the API describes itself: - `GET https://api.fathomcharts.com/v1/indicators`, no authentication: every indicator with the JSON Schemas of its parameters and of its data (see [Indicators](https://fathomcharts.com/en/docs/indicators.md)); - `GET https://api.fathomcharts.com/v1/instruments`, with an API key: the instruments and their tick size (see [Instruments](https://fathomcharts.com/en/docs.md#instruments)); - `GET https://api.fathomcharts.com/v1/offers`, no authentication: the offers and their limits (see [Limits](https://fathomcharts.com/en/docs/limits.md)); - `https://api.fathomcharts.com/docs/json`, no authentication: the OpenAPI 3.1 description of the REST API. ## Instructions for your coding agent Paste this block into the `AGENTS.md` file of your project (read by Codex, Cursor and most agents) or into `CLAUDE.md` (Claude Code): ```md ## Fathom Charts API - Before writing code against the Fathom Charts API, read the documentation index https://fathomcharts.com/llms.txt and the Markdown pages it links to. Do not rely on memory. - Indicator parameters and data shapes: the JSON Schemas of GET https://api.fathomcharts.com/v1/indicators. - Use the TypeScript SDK `@fathom-charts/sdk` for real-time streams and history pagination. - The API key comes from the `FATHOM_CHARTS_API_KEY` environment variable, server-side only: never in code that runs in a browser. ``` ## Good to know - The Markdown pages are generated from the same source as the HTML pages, with every release: they always say the same thing. - Never paste an API key into a conversation with an assistant: give it the name of the environment variable instead (see [Authentication](https://fathomcharts.com/en/docs/authentication.md)). --- Source: https://fathomcharts.com/en/docs/websocket # Real time (WebSocket) A single WebSocket connection can carry several subscriptions. For each one, you first receive the current state, then every change, in order. After a disconnect, you resume exactly where you left off. > The [TypeScript SDK](https://fathomcharts.com/en/docs/quickstart.md#5-use-the-sdk) handles everything on this page for you: initial state, resuming, duplicates and reconnection. Read on if you are writing your own client, or to understand what happens under the hood. ## Connecting ```http wss://stream.fathomcharts.com/v1 ``` - **From a server**: add the `Authorization: Bearer ` header when opening the connection. The `Bearer` scheme is required (a bare key is refused) and is case-insensitive. - **From a browser**: open the connection without headers, then send a [stream token](https://fathomcharts.com/en/docs/authentication.md#browser-access) in an `auth` message, first, within 5 seconds. The WebSocket connection always opens (`101` response) before authentication: a browser cannot read why an opening was refused. A refused key or token therefore closes the connection right after it opens, with code `4001` (see [Closing and reconnecting](#closing-and-reconnecting)). Every message, in both directions, is a JSON object sent in a text frame, whose `t` field gives its type. A message is at most 64 KiB, and your client may send 20 messages per second on average (bursts of 100 allowed). ## Subscribing ```json { "t": "subscribe", "sub": "bt", "instrument": "NQ.front", "indicator": "big-trades", "params": { "minimum": 30 }, "mode": "live", "from": "live" } ``` | Field | Purpose | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sub` | Name you give the subscription (1 to 64 bytes), unique on the connection. Every message about it carries this name. | | `instrument` | A specific contract (`NQZ6`) or the active contract (`NQ.front`). Full list: `GET /v1/instruments`. | | `indicator` | The indicator's ID in the [catalog](https://fathomcharts.com/en/docs/indicators.md). | | `params` | The indicator's parameters. Any you leave out take their default value. | | `mode` | `live` (default): objects in progress and completed. `confirmed`: completed objects only. | | `from` | `"live"` (default): from now on. `{"cursor": "…"}`: [resume](#cursors-and-resuming) after a disconnect. `{"time": "…"}`: start from a [past point in time](#starting-in-the-past), in nanoseconds. | Any other field is refused. To stop: `{"t":"unsubscribe","sub":"bt"}`. No acknowledgement is sent: ignore any messages for that subscription still in flight. You can reuse the same `sub` right away: no message of the old subscription arrives after the `subscribed` of the new one. ### Refusals and errors Every check happens before `subscribed`: a refused subscription receives a single `error` message, carrying its `sub` whenever it can be read, and nothing else. The connection and your other subscriptions carry on unaffected. ```json {"sub":"bt","t":"error","code":"INVALID_PARAMETERS","message":"params.minimum: must be between 1 and 1000000"} ``` `INVALID_PARAMETERS` refusals carry a precise text, for example: | `message` | Cause | | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `` unknown field `x` `` | An unknown field in the message (a typo in `mode` or `from`, for example). | | `mode must be "live" or "confirmed"` | Unknown `mode` value. | | `from must be "live", {"cursor":"…"} or {"time":"…"}` | A malformed `from`, or one holding both `cursor` and `time`. | | `from.time must be Unix nanoseconds` | `from.time` is not a number of nanoseconds (a value in seconds, milliseconds or microseconds, below 10¹⁸, for example). | | `from.time is in the future` | `from.time` is later than now. | | `this stream is delayed by 600 s: from.time must be at least that far in the past` | Delayed data: `from.time` must go back at least as far as the delay. | | `sub already in use on this connection` | The `sub` name is already taken on this connection. | | `params.: …` | An invalid indicator parameter. | The other refusals: an unknown instrument or indicator (`UNKNOWN_INSTRUMENT`, `UNKNOWN_INDICATOR`), a missing right (`FORBIDDEN`, for example a key restricted to other instruments, or `from.time` without the Historical offer or beyond its depth; these history-rights checks come after those of `from`), a limit of your plan reached (`QUOTA_EXCEEDED`: number of subscriptions or custom configurations). An accepted subscription can still receive an `error`, which closes it: - `NOT_COVERED`: the computation needs history older than our data, for example `key-levels` with many `days` or `weeks`; - `QUOTA_EXCEEDED`: the past part of a `from: {"time": …}` exceeds your compute budget; - `CAPACITY`, `UPSTREAM_ERROR`: an incident on our side; try again later. A message the server does not understand (invalid JSON, unknown type, missing or unreadable `sub`) receives an `error` without `sub`. Every cause is listed on the [Errors](https://fathomcharts.com/en/docs/errors.md) page. ## What you receive Every subscription follows the same sequence: ```text subscribed → status warming only if the computation is still being prepared → snapshot from "live" or {"time": …} or reset, then snapshot resume not possible (cursor expired) or the missed messages successful resume: no snapshot → status always, right after → upsert / remove, in order and status, only when the state changes ``` 1. `subscribed`: the subscription is accepted. It gives the contract actually served (`instrument`, for example `NQZ6` for `NQ.front`) and its tick size (`tickSizeNanos`, in billionths). It carries no cursor: the first one comes with the snapshot, or with the first message of a resume; 2. a `warming` `status`, only if the requested computation is not ready yet (a configuration nobody was using, for example). Nothing is sent until it is exact; 3. `snapshot`: the starting state (see [Snapshot content](#snapshot-content)). On a successful resume there is no snapshot: you receive the missed messages directly; 4. `status`: the stream status (live, market closed…), always right after the snapshot, a `reset` or a resume; 5. then an `upsert` or a `remove` for every change, in order. A new `status` only arrives when the state changes: the same state is never sent twice in a row. A real example: the one-minute footprint (`timeframe: 60`, `groupTicks: 4`) on `NQZ6`, in `confirmed` mode, subscribed just before the open on September 24, 2026. The snapshot is empty, then each bar arrives when it closes (here the 13:33 UTC bar): ```json {"sub":"fp","t":"subscribed","topic":"77ca0a9221a93f7eadd15e77f62cec69","instrument":"NQZ6","tickSizeNanos":"250000000"} {"sub":"fp","t":"snapshot","cursor":"20720.111120.0","items":[]} {"sub":"fp","t":"status","state":"live","lagMs":4} {"sub":"fp","cursor":"20720.124235.0","t":"upsert","id":"1790256780000000000","final":true,"ts":"1790256840033077853","data":{"levels":[[122008,14,3],[122012,44,31],[122016,31,37],[122020,16,32],[122024,48,24],[122028,26,24],[122032,40,28],[122036,24,75],[122040,35,46],[122044,14,32],[122048,12,14],[122052,12,9],[122056,20,9],[122060,36,38],[122064,61,58],[122068,29,68],[122072,28,37],[122076,19,20],[122080,30,46],[122084,32,29],[122088,39,42],[122092,87,67],[122096,154,125],[122100,151,163],[122104,149,192],[122108,66,99],[122112,50,81],[122116,22,38],[122120,27,42],[122124,39,72],[122128,10,25],[122132,0,8]],"poc":122104}} ``` In `live` mode, the snapshot would contain the bar in progress (`final: false`), and you would receive a new version of that bar on every trade. A computation still being prepared starts with `warming`, then sends its snapshot as soon as it has caught up: ```json {"sub":"lw","t":"subscribed","topic":"…","instrument":"NQZ6","tickSizeNanos":"250000000"} {"sub":"lw","t":"status","state":"warming","lagMs":0} {"sub":"lw","t":"snapshot","cursor":"20720.124235.4294967295","items":[…]} {"sub":"lw","t":"status","state":"live","lagMs":6} ``` With the market closed (weekend, daily break), the snapshot is followed by `market_closed`, then only `hb` messages arrive until the market reopens: ```json {"sub":"bt","t":"subscribed","topic":"b2af53ed8e6cfc8620b4754c0836e0e2","instrument":"NQZ6","tickSizeNanos":"250000000"} {"sub":"bt","t":"snapshot","cursor":"20721.1032287.4294967295","items":[]} {"sub":"bt","t":"status","state":"market_closed","lagMs":0} ``` To keep your state in sync: - on `snapshot`, **replace** your entire local state for this subscription with its `items`; - on each `upsert`, **replace** object `id` with the one received, or add it. An `upsert` always contains the complete object: a footprint bar carries all its levels, not just the ones that changed; - on each `remove`, delete object `id`; - a `final: false` object can still change; a `final: true` object is complete. The computation moves with the trades: an object in progress when the market closes is only completed by the first trade after it reopens. The last object of the week therefore stays `final: false` until Sunday evening (18:00, New York time). ### Snapshot content The snapshot contains: - the objects in progress (`final: false`); - every object that can still change or be removed, with no limit on their number: all the `key-levels` levels, the `opening-range` range; - then, in the order they were completed, the last 32 completed objects of the current period that started after its first trade. The period is the UTC day of the last trade (by reception time) for `big-trades` and `footprint`, its week (from Saturday 00:00 UTC) for the weekly indicators. An object already in progress when the period began, such as the bar in progress at midnight or the last ones of the previous week, is not among them. In `confirmed` mode, the snapshot only holds completed objects (`final: true`). The snapshot only depends on the point of the stream it describes: a subscription open for hours, a computation that has just started, the snapshot of a `from: {"time": …}` and the [history](https://fathomcharts.com/en/docs/history.md) snapshot are identical at the same point. To fill a chart with more completed objects (big trades, bars, VWAP points), use the history REST API with `"mode": "confirmed"`, then hand off to the stream (see [Hand off to real time](https://fathomcharts.com/en/docs/history.md#hand-off-to-real-time)). ## Message reference Messages you send: | Type (`t`) | Fields | Purpose | | ------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `auth` | `token` | Authenticates a connection opened from a browser, with a stream token. Must be the first message, within 5 s. | | `subscribe` | `sub`, `instrument`, `indicator`, `params`, `mode`, `from` | Opens a subscription. Any other field is refused. | | `unsubscribe` | `sub` | Closes a subscription. No acknowledgement is sent. | | `ping` | — | The server answers `pong`. Optional. | Messages you receive: | Type (`t`) | Fields | Purpose | | ------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `subscribed` | `sub`, `topic`, `instrument`, `tickSizeNanos` | Subscription accepted. `topic` identifies the stream you follow: two identical subscriptions share it. `instrument` is the contract served (`NQZ6` for `NQ.front`), `tickSizeNanos` its tick size in billionths, as a string. No cursor: the first one comes with the snapshot, or with the first message of a resume. | | `snapshot` | `sub`, `cursor`, `items` | Starting state: the objects in progress, those that can still change, then the last 32 completed objects of the period; each with `id`, `final`, `ts` and `data`. | | `upsert` | `sub`, `cursor`, `id`, `final`, `ts`, `data` | Creates object `id`, or replaces it entirely. | | `remove` | `sub`, `cursor`, `id`, `final`, `ts` | Deletes object `id`. | | `status` | `sub`, `state`, `lagMs` | Stream state: live, warming up, source delayed or down, market closed. Sent after every snapshot, reset or resume, then only when the state changes. | | `reset` | `sub`, `reason` | Your local state is no longer valid: discard it, a new snapshot follows. | | `error` | `sub`, `code`, `message` | A subscription was refused or stopped (with its `sub`), or a message was invalid (no `sub` when it cannot be read). | | `hb` | `cursors` | Heartbeat every 5 s, with the cursor of each subscription: everything before it has been sent to you. | | `notice` | `kind` | Announces imminent maintenance (`kind: "reconnect"`). | | `pong` | — | Answer to `ping`. It goes ahead of the queued messages. | In messages, times (`ts`, etc.) are timestamps in nanoseconds since January 1, 1970 (UTC), sent as strings so no precision is lost. Prices are in ticks; averages (such as a VWAP) can have decimals. ## Cursors and resuming Every `snapshot`, `upsert` and `remove` carries a `cursor`, such as `20720.111434.0`: its position in the stream. Cursors never go backwards. They are the same for every client, live and in history. A cursor reads `..`: the UTC day of the trade, its rank within that day, then the rank of the update for that trade. These are three unsigned 32-bit integers: - to compare two cursors, compare their three numbers one by one (the SDK provides `compareCursors`). Read them as a `number` or a `BigInt`, never as a signed 32-bit integer (`int32`), and never compare them as strings (`9` would sort after `10`); - `k = 4294967295` (the largest value) marks the position after every update of that trade: you will see it in the cursors of snapshots and `hb` messages; - never build a cursor yourself: only use cursors you received. **For each subscription, keep the last cursor you received.** The `hb` heartbeat, sent every 5 seconds, moves it forward even when nothing happens: everything before its cursor has been sent to you. ```json {"t":"hb","cursors":{"bt":"20720.112164.4294967295","fp":"20720.124235.0"}} ``` After a disconnect, open a new connection and send the same `subscribe` with that cursor: ```json {"t":"subscribe","sub":"bt","instrument":"NQ.front","indicator":"big-trades","params":{"minimum":30},"mode":"confirmed","from":{"cursor":"20720.112164.4294967295"}} ``` - Short disconnect: you receive `subscribed`, then everything you missed, then a `status`. **No snapshot**: keep your local state as it is. Resuming covers about 15 minutes (less in a very busy market), and only while the computation is kept: it is kept as long as a subscription uses it, then for about 10 minutes after the last one. - Disconnect too long, computation stopped meanwhile, or a cursor that does not exist (beyond the current position of the stream, or corrupted): you receive a `reset` (reason `cursor_expired`) followed by a new snapshot. Start over from that snapshot. After resuming, you may receive a message you already processed. Ignore any `upsert` or `remove` whose cursor is less than or equal to the last one you processed. ## Starting in the past With `from: {"time": "