S&P 500
7,736.45
+12.90 (+0.17%)
Nasdaq
26,427.09
+63.65 (+0.24%)
Dow
54,298.28
-50.84 (-0.09%)
Bitcoin
64,786.20
+679.45 (+1.06%)
simul8or trading simulator logo
FH Finnhub Stock API

Finnhub API +
JavaScript Stock Charts

Fetch real-time and historical stock data from Finnhub and render interactive candlestick charts in the browser. 100% client-side, zero backend required.

Try It Live

Interactive Demo

Enter your Finnhub API key and a ticker symbol to fetch real market data. Use the chart's built-in timeframe selector to switch between intraday, daily, weekly, and monthly views — new data is automatically fetched from Finnhub when you change timeframes.

Chart loaded with sample AAPL daily data. Enter your API key above to fetch live data from Finnhub. Use the chart's built-in timeframe selector to switch views.
Don't have an API key? Sign up for a free Finnhub account at finnhub.io — the free tier includes 60 API calls per minute.
Step by Step

Implementation Guide

Everything you need to get Finnhub data into a chart

01 Get Your Finnhub API Key

Head to finnhub.io/register and create a free account. Once logged in, your API key is displayed on the dashboard. The free tier gives you 60 API calls per minute with access to real-time US stock data, company fundamentals, and more.

02 Include the Chart Library

Add the JavaScript Stock Charts library to your HTML page via CDN. It's zero-dependency — just three script tags and one CSS link. No downloads or installs required.

HTML
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.css">

<!-- Chart container -->
<div id="chart" style="width: 100%; height: 500px;"></div>

<!-- JS (order matters) -->
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/indicators.js"></script>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/patterns.js"></script>
<script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.js"></script>
No downloads needed — the library is served via jsDelivr CDN directly from GitHub. Just include the script tags and you're ready to go.
03 Fetch Data from Finnhub

Use the Stock Candles endpoint to fetch OHLCV data. This works directly in the browser — Finnhub supports CORS on their REST API. Note that Finnhub uses UNIX timestamps in seconds (not milliseconds) for the from and to parameters.

JavaScript
const API_KEY = 'YOUR_FINNHUB_API_KEY';
const ticker  = 'AAPL';
const to      = Math.floor(Date.now() / 1000);           // now (unix seconds)
const from    = to - (365 * 24 * 60 * 60);               // 1 year ago

const url = `https://finnhub.io/api/v1/stock/candle?symbol=${ticker}&resolution=D&from=${from}&to=${to}&token=${API_KEY}`;

const response = await fetch(url);
const json = await response.json();

Finnhub returns data as parallel arrays — each field is a separate array rather than an array of objects:

Finnhub Response
{
  "s": "ok",
  "c": [185.56, 184.25, ...],   // close prices
  "h": [188.44, 185.88, ...],   // high prices
  "l": [183.89, 183.43, ...],   // low prices
  "o": [187.15, 184.22, ...],   // open prices
  "t": [1704153600, 1704240000, ...],  // timestamps (UNIX seconds)
  "v": [58414460, 47299890, ...]       // volume
}
Finnhub Field Description Chart Field
o[]Open prices arrayopen
h[]High prices arrayhigh
l[]Low prices arraylow
c[]Close prices arrayclose
v[]Volume arrayvolume
t[]Timestamps (UNIX seconds)t (ms)
Timestamps: Finnhub returns timestamps in UNIX seconds. The chart library expects milliseconds, so you need to multiply by 1000 when transforming the data.
04 Transform the API Response

Convert Finnhub's parallel arrays into an array of objects that the chart library expects. Loop through the arrays by index and multiply timestamps by 1000 to convert from seconds to milliseconds:

JavaScript
function transformFinnhubData(json) {
    var data = [];
    for (var i = 0; i < json.t.length; i++) {
        var ts = json.t[i] * 1000;  // seconds -> milliseconds
        var d = new Date(ts);
        var date = d.toLocaleString('en-US', {
            timeZone: 'America/New_York',
            year: 'numeric', month: 'numeric', day: 'numeric',
            hour: '2-digit', minute: '2-digit', hour12: false
        }).replace(',', '');
        data.push({
            t: ts, date: date, open: json.o[i], high: json.h[i],
            low: json.l[i], close: json.c[i], volume: json.v[i]
        });
    }
    return data;
}

const chartData = transformFinnhubData(json);
Finnhub returns data sorted chronologically by default, which is exactly what the chart library expects. No additional sorting needed.
05 Render the Chart with Timeframe Switching & Infinite Scroll

Create a StockChart instance with an onReachingStart callback for infinite scroll. Then monitor the chart's built-in timeframe selector to re-fetch data when the user switches timeframes:

JavaScript
var chart = null;
var previousTimeframe = '1day';
var isLoadingMore = false;
var earliestTimestamp = null;
var hasMoreHistory = true;
var timeframeCheckInterval = null;

// Map chart timeframes to Finnhub resolution values
function mapTimeframeToFinnhubParams(tf) {
    var map = {
        '1min':  { resolution: '1',  seconds: 5 * 24 * 3600 },
        '5min':  { resolution: '5',  seconds: 5 * 24 * 3600 },
        '15min': { resolution: '15', seconds: 10 * 24 * 3600 },
        '1hour': { resolution: '60', seconds: 30 * 24 * 3600 },
        '1day':  { resolution: 'D',  seconds: 365 * 24 * 3600 },
        '1week': { resolution: 'W',  seconds: 1095 * 24 * 3600 },
        '1month':{ resolution: 'M',  seconds: 1825 * 24 * 3600 }
    };
    return map[tf] || map['1day'];
}

// Initialize chart
chart = new StockChart('chart', {
    data:      chartData,
    ticker:    ticker,
    chartType: 'candlestick',
    darkMode:  true,
    timeframe: '1day',
    useAfterHoursStyling: true,
    onReachingStart: handleReachingStart
});
earliestTimestamp = chartData[0].t;

// Monitor chart's built-in timeframe selector
setupTimeframeMonitor();

function setupTimeframeMonitor() {
    if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);
    timeframeCheckInterval = setInterval(function() {
        if (chart && chart.timeframe !== previousTimeframe) {
            previousTimeframe = chart.timeframe;
            loadChart(); // re-fetch with new timeframe
        }
    }, 500);
}

// Infinite scroll - fetch older data when user pans left
async function handleReachingStart() {
    if (isLoadingMore || !hasMoreHistory || !earliestTimestamp) return;
    isLoadingMore = true;
    try {
        var p = mapTimeframeToFinnhubParams(chart.timeframe || '1day');
        var toSec = Math.floor(earliestTimestamp / 1000) - 1;
        var fromSec = toSec - p.seconds;
        var url = 'https://finnhub.io/api/v1/stock/candle?symbol=' + ticker
            + '&resolution=' + p.resolution
            + '&from=' + fromSec + '&to=' + toSec
            + '&token=' + API_KEY;
        var resp = await fetch(url);
        var json = await resp.json();
        if (json.s === 'ok' && json.t && json.t.length > 0) {
            var older = transformFinnhubData(json);
            earliestTimestamp = older[0].t;
            chart.prependHistoricalData(older);
        } else {
            hasMoreHistory = false;
            chart.setLoadingHistoricalData(false);
        }
    } catch(e) {
        chart.setLoadingHistoricalData(false);
    } finally {
        isLoadingMore = false;
    }
}

The chart includes a built-in timeframe dropdown — when the user clicks it, we detect the change via setInterval polling on chart.timeframe and automatically re-fetch data from Finnhub. The onReachingStart callback fires when the user pans to the beginning of loaded data, triggering a fetch of older candles that get prepended seamlessly. Technical indicators, pan, zoom, crosshair, volume, and more are all built-in.

Full Example

Complete Working Code

Copy-paste this entire HTML file. Replace YOUR_FINNHUB_API_KEY with your key, open it in a browser, and you'll have a working stock chart in seconds.

finnhub-stock-chart.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Finnhub + JavaScript Stock Charts</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.css">
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            padding: 20px;
            background: #0a0a0f;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            color: #fff;
        }
        h1 { font-size: 1.4rem; margin-bottom: 4px; }
        .subtitle { color: #888; margin-bottom: 16px; }
        .subtitle a { color: #22d3ee; }
        .controls {
            display: flex; gap: 10px; margin-bottom: 12px;
            flex-wrap: wrap; align-items: flex-end;
        }
        .controls label {
            display: block; font-size: 0.7rem;
            text-transform: uppercase; letter-spacing: 1px;
            color: #888; margin-bottom: 4px;
        }
        .controls input {
            background: #16161f; border: 1px solid #333;
            color: #fff; padding: 8px 12px; font-size: 0.9rem;
            border-radius: 2px;
        }
        .controls button {
            background: #10b981; border: none; color: #fff;
            padding: 8px 24px; font-weight: 700; cursor: pointer;
            border-radius: 2px;
        }
        .controls button:hover { background: #059669; }
        .sc-chart-type-button.d-none { display: inline-flex !important; }
        #status { font-size: 0.85rem; color: #888; margin-bottom: 8px; }
        #status.error { color: #ef4444; }
        #status.success { color: #10b981; }
    </style>
</head>
<body>
    <h1>Finnhub + JavaScript Stock Charts</h1>
    <p class="subtitle">
        Powered by <a href="https://simul8or.com/Javascript-Stock-Chart.php">JavaScript Stock Charts</a>
    </p>

    <div class="controls">
        <div>
            <label>API Key</label>
            <input type="text" id="apiKey" placeholder="Your Finnhub API key" style="width:280px">
        </div>
        <div>
            <label>Ticker</label>
            <input type="text" id="ticker" value="AAPL" style="width:100px">
        </div>
        <button onclick="loadChart()">Load Chart</button>
    </div>

    <div id="status"></div>
    <div id="chart" style="width:100%; height:500px;"></div>

    <script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/indicators.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/patterns.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/day-trading-simulator/javascript-stock-charts@main/stock-chart.js"></script>
    <script>
        var chart = null;
        var currentTicker = 'AAPL';
        var previousTimeframe = '1day';
        var timeframeCheckInterval = null;
        var isLoadingMore = false;
        var earliestTimestamp = null;
        var hasMoreHistory = true;

        // Map chart timeframes to Finnhub resolution values
        function mapTimeframeToFinnhubParams(tf) {
            var map = {
                '1min':   { resolution: '1',  seconds: 5 * 24 * 3600 },
                '2min':   { resolution: '5',  seconds: 5 * 24 * 3600 },
                '5min':   { resolution: '5',  seconds: 5 * 24 * 3600 },
                '15min':  { resolution: '15', seconds: 10 * 24 * 3600 },
                '30min':  { resolution: '30', seconds: 15 * 24 * 3600 },
                '1hour':  { resolution: '60', seconds: 30 * 24 * 3600 },
                '60min':  { resolution: '60', seconds: 30 * 24 * 3600 },
                '4hour':  { resolution: '60', seconds: 90 * 24 * 3600 },
                '1day':   { resolution: 'D',  seconds: 365 * 24 * 3600 },
                '1week':  { resolution: 'W',  seconds: 1095 * 24 * 3600 },
                '1W':     { resolution: 'W',  seconds: 1095 * 24 * 3600 },
                '1month': { resolution: 'M',  seconds: 1825 * 24 * 3600 },
                '1M':     { resolution: 'M',  seconds: 1825 * 24 * 3600 }
            };
            return map[tf] || map['1day'];
        }

        function transformFinnhubData(json) {
            var data = [];
            for (var i = 0; i < json.t.length; i++) {
                var ts = json.t[i] * 1000;
                var d = new Date(ts);
                var date = d.toLocaleString('en-US', {
                    timeZone: 'America/New_York',
                    year: 'numeric', month: 'numeric', day: 'numeric',
                    hour: '2-digit', minute: '2-digit', hour12: false
                }).replace(',', '');
                data.push({
                    t: ts, date: date, open: json.o[i], high: json.h[i],
                    low: json.l[i], close: json.c[i], volume: json.v[i]
                });
            }
            return data;
        }

        // Fetch data from Finnhub and render chart
        async function loadChart() {
            var apiKey = document.getElementById('apiKey').value.trim();
            var ticker = document.getElementById('ticker').value.trim().toUpperCase();
            var status = document.getElementById('status');

            if (!apiKey) { status.textContent = 'Enter your Finnhub API key.'; status.className = 'error'; return; }
            if (!ticker) { status.textContent = 'Enter a ticker symbol.'; status.className = 'error'; return; }

            var timeframe = (chart && chart.timeframe) ? chart.timeframe : '1day';
            var p = mapTimeframeToFinnhubParams(timeframe);

            status.textContent = 'Fetching ' + timeframe + ' data...';
            status.className = '';

            var toSec = Math.floor(Date.now() / 1000);
            var fromSec = toSec - p.seconds;

            var url = 'https://finnhub.io/api/v1/stock/candle?symbol=' + ticker
                + '&resolution=' + p.resolution
                + '&from=' + fromSec + '&to=' + toSec
                + '&token=' + apiKey;

            try {
                var resp = await fetch(url);
                var json = await resp.json();
                if (json.s !== 'ok' || !json.t || json.t.length === 0) {
                    status.textContent = json.s === 'no_data'
                        ? 'No data for ' + ticker + '.'
                        : 'Error fetching data: ' + (json.s || 'unknown');
                    status.className = 'error'; return;
                }

                var chartData = transformFinnhubData(json);
                if (chart) chart.destroy();
                if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);

                currentTicker = ticker;
                chart = new StockChart('chart', {
                    data: chartData, ticker: ticker,
                    chartType: 'candlestick', darkMode: true,
                    timeframe: timeframe,
                    useAfterHoursStyling: true,
                    onReachingStart: handleReachingStart
                });

                isLoadingMore = false;
                hasMoreHistory = true;
                earliestTimestamp = chartData[0].t;
                previousTimeframe = chart.timeframe || timeframe;
                setupTimeframeMonitor();

                status.textContent = 'Loaded ' + chartData.length + ' bars (' + timeframe + ') for ' + ticker + '.';
                status.className = 'success';
            } catch (err) {
                status.textContent = 'Network error: ' + err.message;
                status.className = 'error';
            }
        }

        // Monitor chart's built-in timeframe selector for changes
        function setupTimeframeMonitor() {
            if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);
            timeframeCheckInterval = setInterval(function() {
                if (chart && chart.timeframe !== previousTimeframe) {
                    previousTimeframe = chart.timeframe;
                    loadChart();
                }
            }, 500);
        }

        // Infinite scroll - fetch older data when user pans to start
        async function handleReachingStart() {
            if (isLoadingMore || !hasMoreHistory || !earliestTimestamp) return;
            isLoadingMore = true;
            var apiKey = document.getElementById('apiKey').value.trim();
            if (!apiKey) { isLoadingMore = false; return; }
            try {
                var p = mapTimeframeToFinnhubParams(chart.timeframe || '1day');
                var toSec = Math.floor(earliestTimestamp / 1000) - 1;
                var fromSec = toSec - p.seconds;
                var url = 'https://finnhub.io/api/v1/stock/candle?symbol=' + currentTicker
                    + '&resolution=' + p.resolution
                    + '&from=' + fromSec + '&to=' + toSec
                    + '&token=' + apiKey;
                var resp = await fetch(url);
                var json = await resp.json();
                if (json.s === 'ok' && json.t && json.t.length > 0) {
                    var older = transformFinnhubData(json);
                    earliestTimestamp = older[0].t;
                    chart.prependHistoricalData(older);
                } else {
                    hasMoreHistory = false;
                    chart.setLoadingHistoricalData(false);
                }
            } catch(e) {
                chart.setLoadingHistoricalData(false);
            } finally {
                isLoadingMore = false;
            }
        }
    </script>
</body>
</html>
Important: Never commit your API key to a public repository. For production apps, use environment variables or a server-side proxy.
Get Started

Ready to build your own chart?

Download the library, grab your Finnhub API key, and start charting.

See more tutorials: JavaScript Stock Charts Documentation  |  Polygon.io API Tutorial

Wait, here's a special offer

Complete your purchase now and save over 20%.

$19 $15
One-time payment · Unlimited forever
One-time offer — won't be shown again
Secure checkout · Instant access · 30-Day Money Back Guarantee
Stripe
PayPal
No thanks, maybe later