The Hyperliquid Info API exposes a candleSnapshot method that returns an array of candle objects for a coin and interval, bounded by a startTime/endTime window. Because each request is bounded, a continuous OHLCV history is built by advancing a cursor across ordered windows, deduplicating on the candle open time t, and explicitly checking for missing intervals. Fetching at the native interval you intend to store avoids the rounding and volume-attribution errors that compound when you resample coarser candles. Historical candles must then be reconciled with the live candle WebSocket channel so the archive's final candle is not frozen at the moment the poll ran. This guide documents the request contract, interval semantics, windowed pagination, a runnable Node.js collector, a reader-filled results table, failure modes, and the tradeoffs of historical depth and request cost.
The candleSnapshot Request Contract and Response Shape
The Hyperliquid Info endpoint documents candleSnapshot as a POST request whose body carries a type of candleSnapshot, a coin, an interval, and a startTime/endTime window. The response is an array of candle objects rather than a single object, which is the detail that drives every pagination decision later in this guide. Treat the Hyperliquid Docs, Info endpoint as the authoritative source for the exact field names and accepted interval strings, because provider copies of the method reference can drift.
Each candle object carries t (open time), T (close time), s (symbol), i (interval), o/h/l/c (open, high, low, close), v (volume), and n (trade count). The open time t is the natural primary key for an archive: it is stable across requests, so it is the correct field to deduplicate on when windows overlap. The close time T is derived from the interval, so it is useful for gap detection but not for identity.
The request is bounded by the window you supply, and the server returns at most the candles it will return in one response. That single sentence is the reason a naive single call cannot produce a multi-year history, and it is the mechanism the rest of this article builds on. For the broader set of surfaces available for market data, see Hyperliquid historical market data API surfaces.
- Request fields: type, coin, interval, startTime, endTime.
- Response: an array of candle objects, not a wrapper object.
- Identity field: t (open time); derived field: T (close time).
- Volume and trade count: v and n, attributed to the candle's own interval.
Interval Semantics and Why Native Intervals Beat Resampling
The interval field selects the candle width, and the documented interval strings map to fixed millisecond widths. When you request a coarser interval and then resample it into a finer one, you are inventing open, high, low, and close values that the venue never published. The high of a 1h candle is not the high of any particular 1m candle inside it, so a resampled 1m series will disagree with the venue's own 1m series at exactly the points a backtest cares about.
Volume attribution is the second casualty of resampling. A 1h candle's v is the total volume over the hour, and splitting it across twelve 1m candles requires an assumption the data does not contain. If your strategy sizes positions on per-minute volume, that assumption becomes a silent source of error. Fetch at the native interval you intend to store, and store the interval string alongside each candle so the provenance is unambiguous.
The same reasoning applies to trade count n. It is a count over the candle's own window, and it cannot be decomposed into finer windows without the underlying trades. If you genuinely need multiple granularities, fetch each one natively rather than deriving one from another. The Hyperliquid funding rate mechanics page is a useful companion when your strategy also consumes funding, because funding is likewise published on its own schedule rather than derived from candles.
Windowed Pagination, Cursor Advancement, and Gap Checking
Because the request is bounded, continuous history is built by issuing ordered windowed requests and advancing a cursor by the interval width in milliseconds. Start the cursor at your desired startTime, request a window, append the returned candles, then set the next window's startTime to the last candle's t plus one interval. This ordering guarantees that you never request the same window twice in the happy path, and it makes the loop trivially resumable if the process dies mid-run.
Deduplicate on the candle open time t rather than on array position. Overlapping windows are common when you resume from a checkpoint or when you deliberately re-request a boundary window to confirm it is complete, and a Map keyed by t collapses those duplicates deterministically. Keep the last write for a given t, because a re-requested boundary candle may have been in progress when it was first fetched.
Gap checking is the step most implementations skip. After deduplication, sort by t and walk the series, asserting that each consecutive pair differs by exactly one interval width. Any pair that differs by more than one interval is a gap, and it is usually a window with no trades rather than a transport failure. Record gaps explicitly instead of silently interpolating, because a backtest that fills a gap with a synthetic candle is testing data that never existed.
- Advance the cursor by interval milliseconds, not by a fixed candle count.
- Deduplicate on t, keeping the most recently fetched candle for that t.
- Sort by t, then assert consecutive deltas equal one interval width.
- Record gaps as data, never interpolate them silently.
Reconciling Historical Candles with the Live Candle WebSocket
A backfill that stops at the current boundary leaves the archive's final candle frozen at the moment the poll ran. The Hyperliquid WebSocket subscriptions documentation describes a candle channel that pushes updates for the in-progress candle as trades arrive, which is exactly the mechanism needed to keep that final candle current. The Hyperliquid WebSocket subscriptions and connection lifecycle page covers the connection lifecycle in more depth.
The reconciliation rule is simple: backfill via candleSnapshot up to the current boundary, then subscribe to the candle channel for the same coin and interval, and replace the in-progress last candle on every update keyed by its open time t. When the in-progress candle closes and a new one opens, the closed candle is already in your archive under its t, and the new candle arrives under a new t. This is why deduplicating on t matters: the WebSocket update and the historical fetch describe the same candle, and the archive must hold one row for it.
The boundary itself is the subtle part. If you fetch a window whose endTime is in the future relative to the venue's clock, the final candle may be partial. Fetch only up to a boundary you are confident is closed, then let the WebSocket own everything after it. This split is the same live-versus-historical boundary that any backfilled history must reconcile with, and it is documented behavior rather than a provider quirk.
A Runnable Node.js Collector for Windowed candleSnapshot Paging
The collector below pages candleSnapshot over a date range, deduplicates by open time, and reports missing intervals. It uses the global fetch available in modern Node.js and a configurable endpoint so you can point it at your own provider. Replace the endpoint with the one you use; the Hyperliquid RPC endpoints (RPC Assistant) page lists options, and RPC pricing explains how request volume maps to cost.
The interval width table is the only place interval semantics are encoded, so keep it in sync with the documented interval strings. The gap report is intentionally verbose: it prints the missing open times so you can decide whether each gap is a no-trade window or a transport failure worth retrying.
const ENDPOINT = process.env.HL_INFO_ENDPOINT || 'https://api.hyperliquid.xyz/info';
const INTERVAL_MS = { '1m': 60000, '5m': 300000, '15m': 900000, '1h': 3600000, '4h': 14400000, '1d': 86400000 };
async function candleSnapshot({ coin, interval, startTime, endTime }) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ type: 'candleSnapshot', req: { coin, interval, startTime, endTime } })
});
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
if (!Array.isArray(body)) throw new Error('Unexpected response: ' + JSON.stringify(body));
return body;
}
async function collect({ coin, interval, startTime, endTime, windowMs }) {
const step = INTERVAL_MS[interval];
if (!step) throw new Error('Unknown interval: ' + interval);
const byOpenTime = new Map();
let requests = 0;
let cursor = startTime;
while (cursor < endTime) {
const windowEnd = Math.min(cursor + windowMs, endTime);
const batch = await candleSnapshot({ coin, interval, startTime: cursor, endTime: windowEnd });
requests += 1;
for (const c of batch) byOpenTime.set(c.t, c);
const last = batch.length ? batch[batch.length - 1].t : cursor;
cursor = Math.max(last + step, cursor + step);
}
const candles = [...byOpenTime.values()].sort((a, b) => a.t - b.t);
const gaps = [];
for (let i = 1; i < candles.length; i += 1) {
const delta = candles[i].t - candles[i - 1].t;
if (delta !== step) gaps.push({ after: candles[i - 1].t, before: candles[i].t, delta });
}
return { candles, gaps, requests };
}
collect({
coin: 'BTC',
interval: '1m',
startTime: Date.parse('2026-09-01T00:00:00Z'),
endTime: Date.parse('2026-09-08T00:00:00Z'),
windowMs: 6 * 3600000
}).then((r) => {
console.log('candles', r.candles.length, 'requests', r.requests, 'gaps', r.gaps.length);
if (r.candles.length) console.log('first', r.candles[0].t, 'last', r.candles[r.candles.length - 1].t);
for (const g of r.gaps.slice(0, 20)) console.log('gap', new Date(g.after).toISOString(), '->', new Date(g.before).toISOString());
}).catch((e) => { console.error(e); process.exit(1); });A Results Table to Fill Against Your Own Endpoint
Provider behavior varies, so the only honest way to characterize your endpoint is to measure it. Run the collector above against your own endpoint and record the values below. Do not treat any number here as a claim about a specific provider; the table is a template for your own observations.
Record the fetch window per run so the numbers are reproducible. If you change the window size, the requests-issued column will change, and that is the point: it shows the cost tradeoff directly.
- First candle returned (ISO timestamp of t).
- Last candle returned (ISO timestamp of t).
- Candles per request (min, median, max across windows).
- Gaps detected (count and the open times).
- Requests issued for the full range.
- Wall-clock duration and any rate-limit responses observed.
Failure Modes and Troubleshooting
An empty array for a window usually means no trades occurred in that window, not that the request failed. Confirm by checking whether the window falls in a low-liquidity period, and by re-requesting a neighboring window that you know has trades. If the neighboring window returns candles and the empty window is genuinely quiet, record it as a gap rather than retrying indefinitely.
A rate-limit error when windows are requested too fast is the most common operational failure. The Info API returns an error object, and the JSON-RPC 2.0 Specification is the authoritative reference for the error-object envelope shape that such responses follow. Back off and retry rather than hammering the endpoint; the Hyperliquid API rate limits: Info vs Exchange page separates the two surfaces, which matters because they are budgeted differently.
A coin-name case or naming mismatch returns an empty result that looks identical to a no-trade window. Coin identifiers are case-sensitive in practice, so normalize your input and verify against a known-good request before concluding the window is empty. A final candle that appears to lag is usually a candle fetched mid-interval: its close, high, low, and volume are still moving. The fix is the reconciliation step above, not a retry.
- Empty array: check for a no-trade window before assuming failure.
- Rate-limit error: back off, then retry; do not parallelize blindly.
- Naming mismatch: normalize coin case and verify with a known-good request.
- Lagging final candle: it was fetched mid-interval; let the WebSocket own it.
Historical Depth, Request Cost, and Provenance Tradeoffs
Historical depth is a documented constraint you should discover empirically rather than assume. The practical ceiling on how far back candleSnapshot will serve data is discovered by trial, as the ccxt GitHub issue on fetch_ohlcv limits illustrates, and it can differ from the depth available through other surfaces. Probe backward with a single window before committing to a multi-year backfill plan.
Request cost scales inversely with window size. Many small windows give you finer control over retries and resumability but multiply the number of requests, which interacts with rate limits and with whatever your provider meters. Large windows reduce request count but make a single failure more expensive to retry. The right window size is the largest one that still returns complete data for your interval, which is exactly what the results table above measures.
Record the fetch window per candle in your archive. A candle fetched in a window that ended mid-interval may have been partial at fetch time, and without the window metadata you cannot tell later whether a low volume figure was real or an artifact of when you asked. Provenance is cheap to store and expensive to reconstruct. For the broader decision of which data surface to use, see the OnFinality Learn hub and the API service overview.
Next Steps for a Production OHLCV Archive
Move from a one-shot script to a scheduled backfill plus a live tail. Run the windowed collector on a schedule to extend history, and run the WebSocket reconciliation continuously so the boundary candle stays current. Persist the cursor and the last closed candle's t so a restart resumes without re-fetching the entire range.
Add a gap-repair pass that re-requests only the windows surrounding recorded gaps, and keep the gap report as a first-class artifact rather than a log line. If your strategy consumes funding or oracle prices alongside candles, the Hyperliquid oracle prices and the builder auction page covers those Info API surfaces. Finally, review RPC pricing before scaling request volume, and confirm your endpoint choice against Hyperliquid RPC endpoints (RPC Assistant).
- Schedule the backfill; run the WebSocket tail continuously.
- Persist the cursor and last closed t for resumable restarts.
- Repair gaps by re-requesting only the affected windows.
- Store the fetch window per candle for provenance.