Using the API

Exports

For a long range (a full day of footprint, months of data for a backtest), an export replaces pagination: one request, and results stream in as they are computed.

Exports come with the Historical offer and require a key with the export scope.

When to use an export

You needUse
A few thousand results, for displayA history query
A full day or more, a backtest, a databaseAn export
Load the past, then follow the marketfrom: {"time": …} on the WebSocket

Start an export

The body is the same as a history query, without limit, cursor or untilCursor. Here, a full day of footprint:

BASH
curl -sS https://api.fathomcharts.com/v1/exports \
  -H "Authorization: Bearer $FATHOM_CHARTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"instrument":"NQZ6","indicator":"footprint","params":{"timeframe":60,"groupTicks":4},"from":"1790208000000000000","to":"1790294400000000000"}' \
  | zstd -d > footprint-NQZ6-2026-09-24.ndjson

The response is zstd-compressed and curl does not decompress it on its own, hence the zstd -d. To keep the compressed file, replace | zstd -d > with -o footprint-NQZ6-2026-09-24.ndjson.zst.

The compute cost of the whole range is charged as soon as the export starts; estimate it first if needed. The downloaded volume counts against your monthly export volume. If either one is used up, the export is rejected with 429 QUOTA_EXCEEDED.

File format

One JSON line per update, in order, in the same shape as a history query result. For example, the 13:33 UTC bar at its close:

JSON
{"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}}

As with history, the file also contains intermediate versions (final: false): filter on final: true if you only care about the finished result.

Lines arrive as soon as they are computed. Process them as they come rather than loading the whole file into memory.

With the SDK

FathomChartsRest.exportNdjson() decompresses, splits into lines and, if the connection drops, resumes automatically after the last line received:

TS
import { createWriteStream } from 'node:fs';
import { FathomChartsRest } from '@fathom-charts/sdk';

const rest = new FathomChartsRest({ baseUrl: 'https://api.fathomcharts.com', apiKey: process.env.FATHOM_CHARTS_API_KEY! });
const out = createWriteStream('footprint-NQZ6-2026-09-24.ndjson');

for await (const lines of rest.exportNdjson({
  instrument: 'NQZ6',
  indicator: 'footprint',
  params: { timeframe: 60, groupTicks: 4 },
  from: '1790208000000000000',
  to: '1790294400000000000',
})) {
  if (!out.write(lines)) await new Promise((resolve) => out.once('drain', resolve));
}
out.end();

In Node, decompression requires Node 22.15 or later. To save the compressed file as is, without automatic resume, use FathomChartsRest.export(): it returns the byte stream (body), the suggested file name (filename) and the resume token.

Without the SDK, in Node

TS
import { request } from 'node:https';
import { createZstdDecompress } from 'node:zlib';
import { createInterface } from 'node:readline';

const body = JSON.stringify({
  instrument: 'NQZ6',
  indicator: 'footprint',
  params: { timeframe: 60, groupTicks: 4 },
  from: '1790208000000000000',
  to: '1790294400000000000',
});

const req = request('https://api.fathomcharts.com/v1/exports', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FATHOM_CHARTS_API_KEY}`,
    'Content-Type': 'application/json',
  },
});

req.on('response', async (res) => {
  if (res.statusCode !== 200) {
    // Errors are application/problem+json, not compressed.
    let problem = '';
    for await (const chunk of res) problem += chunk;
    throw new Error(problem);
  }
  const lines = createInterface({ input: res.pipe(createZstdDecompress()), crlfDelay: Infinity });
  for await (const line of lines) {
    const m = JSON.parse(line);
    if (m.t === 'upsert' && m.final) console.log(m.cursor, m.id);
  }
});

req.end(body);

Resume after a disconnect

A long export can be cut off (network drop, maintenance): you see a read error or an incomplete compressed file. No need to start over:

  1. keep the X-Export-Token header of the response (valid for 24 hours);
  2. note the cursor of the last complete line you received, and ignore any truncated line at the end;
  3. send exactly the same request again, adding resume:
JSON
{
  "instrument": "NQZ6",
  "indicator": "footprint",
  "params": { "timeframe": 60, "groupTicks": 4 },
  "from": "1790208000000000000",
  "to": "1790294400000000000",
  "resume": { "token": "<value of X-Export-Token>", "after": "20720.124235.0" }
}

The export picks up right after that line. The new response is a new compressed file: decompress each response separately, then concatenate the lines. The result is identical to an uninterrupted export. Resuming costs no extra compute units; only the downloaded bytes count against your export volume.