Your page
Draws the chart with tiingo-feed.js. It asks the relay for candles and listens for live prices. It never sees your Tiingo token.
Chart live and historical Tiingo data in an interactive candlestick chart. A small Node relay keeps your token off the browser and handles the connections Tiingo won't accept from a web page.
Real Tiingo data, no key needed. Switch timeframes from the chart's clock menu and drag left to load older bars. Prices stream in during market hours.
Market data provided by Tiingo. Intraday bars are IEX prices, including extended hours.
Tiingo is built for server-to-server use. A web page can't call it directly, so the kit puts a small relay between the two.
Draws the chart with tiingo-feed.js. It asks the relay for candles and listens for live prices. It never sees your Tiingo token.
relay.js holds your token, calls Tiingo's REST API for history, and keeps one Tiingo WebSocket open for every viewer.
Serves IEX intraday bars, end-of-day history, and a live price stream that every Tiingo plan includes.
If you try to connect from the browser, two things stop you:
| Browser call | What Tiingo does |
|---|---|
WebSocket to wss://api.tiingo.com/iex | Refuses the handshake from any web page: 403 Cross origin websockets not allowed |
fetch() to the REST API | Responds without CORS headers, so the browser blocks your code from reading the response |
A relay also means your token never ships to the browser, where anyone could copy it from the network tab.
Create a free account at tiingo.com and copy your token from the API token page. Every plan includes the IEX price stream the relay uses.
Download the kit and unzip it. It contains the relay, the chart library, tiingo-feed.js, and an example page. The relay has no dependencies, so there's nothing to install beyond Node 22 or newer.
cd stock-chart-tiingo-kit TIINGO_TOKEN=your_token node relay.js
cd stock-chart-tiingo-kit $env:TIINGO_TOKEN = "your_token" node relay.js
Open http://localhost:8787. The relay serves the example page itself, so you'll see a live AAPL chart right away.
Include the chart library, then tiingo-feed.js, and point a TiingoFeed at your relay. The feed creates the chart, loads history for each timeframe, loads older bars as you pan left, and rolls live prices into the last candle.
<link rel="stylesheet" href="stock-chart.css">
<div id="chart" style="width: 100%; height: 560px;"></div>
<script src="indicators.js"></script>
<script src="patterns.js"></script>
<script src="stockchart.js"></script>
<script src="tiingo-feed.js"></script>
<script>
const feed = new TiingoFeed('chart', {
relay: 'http://localhost:8787',
symbol: 'AAPL',
timeframe: '5min',
onStatus: (status) => console.log(status.state, status.text)
});
// Later: feed.setSymbol('MSFT'); feed.setTimeframe('1day');
</script>
Page on another origin? If your page isn't served by the relay, start the relay with ALLOWED_ORIGINS=http://localhost:3000 (comma-separate several). The relay only answers browser calls from origins on that list.
Both endpoints work with any chart or framework, not only this library.
| Endpoint | Returns |
|---|---|
GET /candles?symbol=AAPL&timeframe=5min | { ok, data, error }, where data is [{ t, open, high, low, close, volume }], oldest first. Add &end=<ms> for the bars before that time. |
GET /stream?symbols=AAPL,MSFT | Server-Sent Events. Each message is { symbol, price, time }. A status event reports live, connecting, or error. |
| Timeframe | Per request | Tiingo source |
|---|---|---|
1min 5min 15min 30min 1hour | 3 to 90 days | IEX intraday prices, including extended hours. IEX volume is only a small slice of the market's, so volume is left out. |
1day 1week 1month | 2 to 25 years | End-of-day prices, adjusted for splits and dividends, with volume |
| Option | Type | Default | Description |
|---|---|---|---|
relay | String | '' | Relay base URL. An empty string means the page's own origin. |
symbol | String | 'AAPL' | Ticker to chart |
timeframe | String | '5min' | Any timeframe from the table above |
chartOptions | Object | {} | Passed to new StockChart(): darkMode, indicators, movingAverages, and more |
onStatus | Function | — | Called with { state, text }. State is loading, ready, live, reconnecting, or error. |
loadCandles | Function | relay | Fetch history yourself. See below. |
subscribe | Function | relay | Bring your own live feed. See below. |
feed.setSymbol('MSFT')Chart another tickerfeed.setTimeframe('1day')Reload at another timeframefeed.chartThe StockChart instancefeed.destroy()Stop streaming and remove the chartAlready have a server that talks to Tiingo? Skip the relay and hand the feed two functions. The demo on this page works this way: it reads candles from simul8or's own endpoint and live prices from simul8or's existing Tiingo stream.
const feed = new TiingoFeed('chart', {
symbol: 'AAPL',
timeframe: '5min',
// Return [{ t, open, high, low, close, volume }], oldest first.
// `end` is set when the chart wants the bars before that time.
loadCandles: async ({ symbol, timeframe, end }) => {
const res = await fetch(`/my-api/candles?symbol=${symbol}&tf=${timeframe}&end=${end || ''}`);
return res.json();
},
// Call onTick({ price, time }) for each update. Return a function that stops it.
subscribe: (symbol, onTick, onState) => {
const socket = new WebSocket('wss://my-server.example/prices?symbol=' + symbol);
socket.onopen = () => onState('live');
socket.onclose = () => onState('reconnecting');
socket.onmessage = (e) => onTick(JSON.parse(e.data));
return () => socket.close();
}
});
HOST=0.0.0.0 and put it behind nginx, Caddy, or IIS. Disable response buffering for /stream so prices aren't held back.This is index.html from the kit, the page the relay serves at http://localhost:8787.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tiingo + Stock Charts</title>
<link rel="stylesheet" href="stock-chart.css">
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 20px; background: #0a0a0f; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
h1 { font-size: 1.3rem; margin: 0 0 4px; }
.sub { color: #8b8b99; margin: 0 0 16px; font-size: 0.9rem; }
.sub a { color: #22d3ee; text-decoration: none; }
form { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
form input { background: #16161f; border: 1px solid #333; color: #fff; padding: 8px 12px; font-size: 0.95rem; border-radius: 2px; width: 120px; text-transform: uppercase; }
form button { background: #10b981; border: 0; color: #fff; padding: 8px 20px; font-weight: 700; border-radius: 2px; cursor: pointer; }
#status { font-size: 0.85rem; color: #8b8b99; min-height: 1.3em; margin-bottom: 8px; }
#status[data-state="live"] { color: #10b981; }
#status[data-state="error"] { color: #ef4444; }
#chart { width: 100%; height: 560px; }
/* The chart hides tools it doesn't use with .d-none. */
.d-none { display: none !important; }
</style>
</head>
<body>
<h1>Tiingo + Stock Charts</h1>
<p class="sub">Market data provided by <a href="https://www.tiingo.com">Tiingo</a>. Chart by <a href="https://simul8or.com/Javascript-stock-chart.php">JavaScript Stock Charts</a>.</p>
<form id="symbolForm">
<input id="symbol" value="AAPL" aria-label="Ticker symbol" autocomplete="off">
<button type="submit">Load</button>
</form>
<div id="status" role="status"></div>
<div id="chart"></div>
<script src="indicators.js"></script>
<script src="patterns.js"></script>
<script src="stockchart.js"></script>
<script src="tiingo-feed.js"></script>
<script>
var statusEl = document.getElementById('status');
var feed = new TiingoFeed('chart', {
relay: '', // '' = this page's origin. Use the relay's URL if the page lives elsewhere.
symbol: 'AAPL',
timeframe: '5min',
chartOptions: { darkMode: true },
onStatus: function (status) {
var labels = { loading: 'Loading...', ready: 'Waiting for live prices', live: 'Live', reconnecting: 'Reconnecting...' };
statusEl.textContent = status.text || labels[status.state] || '';
statusEl.dataset.state = status.state;
}
});
document.getElementById('symbolForm').addEventListener('submit', function (event) {
event.preventDefault();
feed.setSymbol(document.getElementById('symbol').value);
});
</script>
</body>
</html>
Download the kit, add your token, and run one command.