Get started
Integrations

Stock market data in JavaScript,
with plain fetch.

Node.js 18 and later ship fetch, which is all the OHLC REST API needs. This guide covers typed requests, pagination, keeping the key on your server and streaming delayed quotes over WebSocket.

Built-in fetch

No SDK or dependency: bearer auth, JSON envelopes and cursor pagination with the platform fetch.

Typed with TypeScript

Response fields come straight from the OpenAPI spec, which can also generate a full client.

Key stays server-side

Call OHLC from your backend and expose only the data your frontend needs.

WebSocket quotes

Pro and above can stream delayed quote snapshots from wss://api.ohlc.io/v1/stream: exchange listings 15 minutes delayed, crypto and currency pairs every few minutes.

A typed fetch helper

  1. Create an API key and set it as OHLC_API_KEY in your server's environment.
  2. Use the helper below for every request: it adds the header, retries rate limits and throws the API's problem document on failure.
  3. Pass lists as one comma-separated value, such as symbols: "XNAS:AAPL,XNYS:JNJ", and dates as YYYY-MM-DD.
ohlc.tsJavaScript
// ohlc.ts — server-side only
const API = "https://api.ohlc.io/v1";

export type DailyBar = {
  date: string;
  instrument_id: string;
  symbol: string;
  exchange_mic: string;
  currency_code: string;
  open: string | null;
  high: string | null;
  low: string | null;
  close: string | null;
  volume: string | null;
  trade_count: number | null;
  adjustment?: "split" | "total_return";
  factor_set_version?: number;
};

export type Page<T> = {
  data: T[];
  meta: {
    timezone: "UTC";
    as_of: string;
    next_cursor: string | null;
    errors?: { symbol: string | null; instrument_id: string | null; code: string; status: number; detail: string }[];
  };
};

export async function ohlc<T>(path: string, params: Record<string, string | number> = {}): Promise<Page<T>> {
  const url = new URL(API + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, String(value));
  for (let attempt = 0; ; attempt++) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.OHLC_API_KEY}` },
    });
    if (response.ok) return response.json();
    const wait = Number(response.headers.get("retry-after") ?? 1);
    if (response.status === 429 && wait <= 60 && attempt < 4) {
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue;
    }
    const problem = await response.json().catch(() => ({}));
    throw Object.assign(new Error(`${response.status} ${problem.code}: ${problem.detail}`), { status: response.status, problem });
  }
}

Errors are RFC 9457 problem documents with a stable code, such as symbol-not-resolved or daily-credit-limit. A Retry-After longer than a minute on a 429 usually means the daily credit allowance is spent until 00:00 UTC, so the helper stops instead of waiting.

Pagination with an async generator

Every daily bar in a rangeJavaScript
export async function* paginate<T>(path: string, params: Record<string, string | number>) {
  let cursor: string | null = null;
  do {
    const page: Page<T> = await ohlc<T>(path, cursor ? { ...params, cursor } : params);
    yield* page.data;
    cursor = page.meta.next_cursor;
  } while (cursor);
}

for await (const bar of paginate<DailyBar>("/v1/eod", {
  symbols: "XNAS:AAPL,XNYS:JNJ",
  from: "2026-01-02",
  to: "2026-10-02",
  adjustment: "split",
  limit: 500,
})) {
  console.log(bar.symbol, bar.date, bar.close);
}

Cursors expire after an hour and are bound to the query, so keep the other parameters unchanged between pages. Prices are decimal strings; use a decimal library if you need exact arithmetic, or Number() for charts.

Keep the API key on the server

A key in browser code can be copied by anyone who opens the page. Call OHLC from your backend and return only what the page needs. Browser access is possible only with a key that is explicitly browser-enabled and restricted to allowed origins, and even then a proxy is simpler to reason about.

A minimal Express proxyJavaScript
import express from "express";
import { ohlc, type DailyBar } from "./ohlc";

const app = express();

app.get("/api/closes/:symbol", async (req, res) => {
  try {
    const page = await ohlc<DailyBar>("/v1/eod", {
      symbols: req.params.symbol,
      from: String(req.query.from ?? "2026-01-02"),
      limit: 500,
    });
    res.json(page.data.map(({ date, close }) => ({ date, close })));
  } catch (error: any) {
    res.status(error.status ?? 500).json({ error: error.problem?.code ?? "unexpected" });
  }
});

app.listen(3000);

Stream delayed quotes over WebSocket

On Pro and above, wss://api.ohlc.io/v1/stream pushes a quote frame whenever a new snapshot changes a subscribed quote: exchange listings are delayed at least 15 minutes, and crypto and currency pairs are snapshotted every few minutes. It is not real-time. Authenticate within 10 seconds, wait for authenticated, subscribe, and send ping well inside each 120-second lease. Node.js 22 and later include a global WebSocket.

Delayed quote stream (Node.js 22+)JavaScript
const socket = new WebSocket("wss://api.ohlc.io/v1/stream");
let heartbeat;

socket.addEventListener("open", () => {
  socket.send(JSON.stringify({ type: "authenticate", key: process.env.OHLC_API_KEY }));
});

socket.addEventListener("message", (event) => {
  const message = JSON.parse(event.data);
  if (message.type === "authenticated") {
    socket.send(JSON.stringify({ type: "subscribe", symbols: ["XNAS:AAPL", "XNYS:JNJ"] }));
    heartbeat = setInterval(() => socket.send(JSON.stringify({ type: "ping" })), 60_000);
  } else if (message.type === "quotes") {
    for (const quote of message.data) console.log(quote.symbol, quote.last_price, quote.last_at);
  } else if (message.type === "error") {
    console.error(message.detail);
  }
});

socket.addEventListener("close", () => clearInterval(heartbeat));

There is no replay: on disconnect, reconnect with backoff, authenticate and resubscribe. A subscribe message takes at most 100 symbols, and your plan's symbol limit counts the union across all of the account's connections. Pro streams up to 250 symbols.

Generate a client from the OpenAPI spec

The full contract is published at /specs/ohlc-openapi.json, with every parameter, enum and response schema. Generators such as openapi-typescript turn it into types for every endpoint, so a typo in adjustment or a missing required interval fails at compile time rather than as a 422. The same spec feeds the API reference and the Postman collection, so the three stay in step.

Practical tips for production

  • Cache daily bars. They change only when a new session is published or, for adjusted series, when factor_set_version moves. The dataset endpoints also return an ETag and accept If-None-Match; a 304 costs no credits, so revalidating an unchanged result is free.
  • Log the X-Request-ID response header with every error; it is the fastest way to trace a request with support.
  • Watch X-Credits-Remaining. The daily credit allowance is a hard cap with no overage charges, so a runaway loop stops at 429 until 00:00 UTC rather than producing a bill.
  • Tolerate new response fields. The API adds fields without a version change; read the ones you need and ignore the rest.

Questions, answered.

Is there an OHLC JavaScript SDK?

Not today. The API works with the built-in fetch, and the OpenAPI spec at /specs/ohlc-openapi.json can generate a typed TypeScript client.

Can I call the OHLC API from the browser?

Only with a browser-enabled key restricted to allowed origins. Calling it from your server and returning the needed data is safer and keeps credits under your control.

Does the WebSocket stream real-time prices?

No. It pushes snapshots as they arrive: exchange quotes delayed at least 15 minutes, and crypto and currency pairs every few minutes. It requires a Pro plan or above.

How do I page through results in Node.js?

Read meta.next_cursor from each response and pass it as cursor with the same parameters until it is null; the async generator above does this.

Which Node.js version do I need?

Node.js 18 or later for fetch, and Node.js 22 or later for the built-in WebSocket used in the streaming example.

A little JSON.
A lot of possibility.

Explore the API