Fetch real-time and historical stock data from Financial Modeling Prep and render interactive candlestick charts in the browser. 100% client-side, zero backend required.
Enter your FMP 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 FMP when you change timeframes.
Everything you need to get FMP data into a chart
Head to financialmodelingprep.com/register and create a free account. Once logged in, your API key is displayed on the dashboard. The free tier gives you 250 API calls per day with access to historical stock data, company fundamentals, and more. See the full API docs for all available endpoints.
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.
<!-- 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>
FMP uses two different endpoints depending on the timeframe you need. For intraday data (1min through 4hour), use the historical chart endpoint. For daily and longer timeframes, use the historical price full endpoint. Both work directly in the browser — FMP supports CORS on their REST API.
const API_KEY = 'YOUR_FMP_API_KEY';
const ticker = 'AAPL';
const from = '2026-03-06'; // YYYY-MM-DD
const to = '2026-03-11';
// Intraday: 1min, 5min, 15min, 30min, 1hour, 4hour
const url = `https://financialmodelingprep.com/stable/historical-chart/5min?symbol=${ticker}&from=${from}&to=${to}&apikey=${API_KEY}`;
const response = await fetch(url);
const json = await response.json();
// json is a FLAT ARRAY: [ { date, open, high, low, close, volume }, ... ]
// Daily / full history
const url = `https://financialmodelingprep.com/stable/historical-price-eod/full?symbol=${ticker}&from=${from}&to=${to}&apikey=${API_KEY}`;
const response = await fetch(url);
const json = await response.json();
// json is an OBJECT: { symbol: "AAPL", historical: [ { date, open, high, ... }, ... ] }
The intraday endpoint returns a flat JSON array:
[
{ "date": "2026-03-11 16:00:00", "open": 260.14, "high": 260.50, "low": 259.80, "close": 260.25, "volume": 14523 },
{ "date": "2026-03-11 15:55:00", "open": 260.00, "high": 260.30, "low": 259.70, "close": 260.10, "volume": 12345 },
...
]
The daily endpoint returns an object with a historical array:
{
"symbol": "AAPL",
"historical": [
{ "date": "2026-03-11", "open": 260.14, "high": 262.48, "low": 258.90, "close": 261.35, "volume": 45123456, "adjClose": 261.35, ... },
{ "date": "2026-03-10", "open": 258.50, "high": 260.20, "low": 257.10, "close": 259.80, "volume": 38456789, ... },
...
]
}
| FMP Field | Description | Chart Field |
|---|---|---|
date | Date/time string | t (ms) / date |
open | Open price (number) | open |
high | High price (number) | high |
low | Low price (number) | low |
close | Close price (number) | close |
volume | Trading volume (number) | volume |
historical key) — your code must handle both.
parseFloat() is needed. FMP also has no weekly or monthly endpoints — for those timeframes, fetch daily data with a wider date range.
Use a single transform function that handles both intraday and daily response formats. The isIntraday flag determines whether to treat the response as a flat array or extract the historical property. In both cases, reverse the data to chronological order:
function transformFMPData(json, isIntraday) {
var arr = isIntraday ? json : json.historical;
if (!arr || arr.length === 0) return [];
// Reverse from newest-first to oldest-first (chronological)
return arr.slice().reverse().map(function(bar) {
// Intraday dates include time: "2026-03-11 16:00:00"
// Daily dates are date-only: "2026-03-11"
var ts = bar.date.includes(' ')
? new Date(bar.date).getTime()
: new Date(bar.date + 'T12:00:00').getTime();
return {
t: ts, date: bar.date,
open: bar.open, high: bar.high,
low: bar.low, close: bar.close, volume: bar.volume
};
});
}
var chartData = transformFMPData(json, true); // for intraday
var chartData = transformFMPData(json, false); // for daily
bar.date directly. The .slice().reverse() creates a new reversed array without mutating the original response data.
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. FMP requires building different URLs for intraday vs. daily timeframes:
var chart = null;
var previousTimeframe = '1day';
var isLoadingMore = false;
var earliestTimestamp = null;
var hasMoreHistory = true;
var timeframeCheckInterval = null;
// Helper: format Date as YYYY-MM-DD
function fmtDate(d) { return d.toISOString().split('T')[0]; }
// Map chart timeframes to FMP params
function mapTimeframeToFMPParams(tf) {
var map = {
'1min': { type: 'intraday', interval: '1min', days: 5 },
'2min': { type: 'intraday', interval: '5min', days: 5 },
'5min': { type: 'intraday', interval: '5min', days: 5 },
'15min': { type: 'intraday', interval: '15min', days: 10 },
'30min': { type: 'intraday', interval: '30min', days: 15 },
'1hour': { type: 'intraday', interval: '1hour', days: 30 },
'60min': { type: 'intraday', interval: '1hour', days: 30 },
'4hour': { type: 'intraday', interval: '4hour', days: 90 },
'1day': { type: 'daily', interval: null, days: 365 },
'1week': { type: 'daily', interval: null, days: 1095 },
'1W': { type: 'daily', interval: null, days: 1095 },
'1month': { type: 'daily', interval: null, days: 1825 },
'1M': { type: 'daily', interval: null, days: 1825 }
};
return map[tf] || map['1day'];
}
// Build the correct URL based on timeframe type
function buildFMPUrl(ticker, p, apiKey, fromDate, toDate) {
if (p.type === 'intraday') {
return 'https://financialmodelingprep.com/stable/historical-chart/'
+ p.interval + '?symbol=' + ticker
+ '&from=' + fromDate + '&to=' + toDate + '&apikey=' + apiKey;
} else {
return 'https://financialmodelingprep.com/stable/historical-price-eod/full?symbol=' + ticker
+ '&from=' + fromDate + '&to=' + toDate + '&apikey=' + apiKey;
}
}
// 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 = mapTimeframeToFMPParams(chart.timeframe || '1day');
var earliest = new Date(earliestTimestamp);
var toDate = fmtDate(new Date(earliest.getTime() - 86400000));
var fromDate = fmtDate(new Date(earliest.getTime() - p.days * 86400000));
var url = buildFMPUrl(ticker, p, API_KEY, fromDate, toDate);
var resp = await fetch(url);
var json = await resp.json();
var isIntraday = p.type === 'intraday';
var arr = isIntraday ? json : (json.historical || []);
if (Array.isArray(arr) && arr.length > 0) {
var older = transformFMPData(json, isIntraday);
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 FMP. The onReachingStart callback fires when the user pans to the beginning of loaded data, triggering a fetch of older candles that get prepended seamlessly. Error handling checks for Array.isArray(json) on intraday responses and json.historical on daily responses. Technical indicators, pan, zoom, crosshair, volume, and more are all built-in.
Copy-paste this entire HTML file. Replace YOUR_FMP_API_KEY with your key, open it in a browser, and you'll have a working stock chart in seconds.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Financial Modeling Prep + 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>Financial Modeling Prep + 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 FMP 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;
function fmtDate(d) { return d.toISOString().split('T')[0]; }
function mapTimeframeToFMPParams(tf) {
var map = {
'1min': { type: 'intraday', interval: '1min', days: 5 },
'2min': { type: 'intraday', interval: '5min', days: 5 },
'5min': { type: 'intraday', interval: '5min', days: 5 },
'15min': { type: 'intraday', interval: '15min', days: 10 },
'30min': { type: 'intraday', interval: '30min', days: 15 },
'1hour': { type: 'intraday', interval: '1hour', days: 30 },
'60min': { type: 'intraday', interval: '1hour', days: 30 },
'4hour': { type: 'intraday', interval: '4hour', days: 90 },
'1day': { type: 'daily', interval: null, days: 365 },
'1week': { type: 'daily', interval: null, days: 1095 },
'1W': { type: 'daily', interval: null, days: 1095 },
'1month': { type: 'daily', interval: null, days: 1825 },
'1M': { type: 'daily', interval: null, days: 1825 }
};
return map[tf] || map['1day'];
}
function buildFMPUrl(ticker, p, apiKey, fromDate, toDate) {
if (p.type === 'intraday') {
return 'https://financialmodelingprep.com/stable/historical-chart/'
+ p.interval + '?symbol=' + ticker
+ '&from=' + fromDate + '&to=' + toDate + '&apikey=' + apiKey;
} else {
return 'https://financialmodelingprep.com/stable/historical-price-eod/full?symbol=' + ticker
+ '&from=' + fromDate + '&to=' + toDate + '&apikey=' + apiKey;
}
}
function transformFMPData(json, isIntraday) {
var arr = isIntraday ? json : json.historical;
if (!arr || arr.length === 0) return [];
return arr.slice().reverse().map(function(bar) {
var ts = bar.date.includes(' ')
? new Date(bar.date).getTime()
: new Date(bar.date + 'T12:00:00').getTime();
return {
t: ts, date: bar.date,
open: bar.open, high: bar.high,
low: bar.low, close: bar.close, volume: bar.volume
};
});
}
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 FMP 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 = mapTimeframeToFMPParams(timeframe);
var isIntraday = p.type === 'intraday';
status.textContent = 'Fetching ' + timeframe + ' data...';
status.className = '';
var now = new Date();
var toDate = fmtDate(now);
var fromDate = fmtDate(new Date(now.getTime() - p.days * 86400000));
var url = buildFMPUrl(ticker, p, apiKey, fromDate, toDate);
try {
var resp = await fetch(url);
var json = await resp.json();
// Error handling: check both response formats
if (isIntraday) {
if (!Array.isArray(json) || json.length === 0) {
status.textContent = json['Error Message'] || typeof json === 'string'
? 'Error: ' + (json['Error Message'] || json)
: 'No intraday data for ' + ticker + '.';
status.className = 'error'; return;
}
} else {
if (!json.historical || json.historical.length === 0) {
status.textContent = json['Error Message'] || typeof json === 'string'
? 'Error: ' + (json['Error Message'] || json)
: 'No daily data for ' + ticker + '.';
status.className = 'error'; return;
}
}
var chartData = transformFMPData(json, isIntraday);
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 = '';
status.className = '';
} catch (err) {
status.textContent = 'Network error: ' + err.message;
status.className = 'error';
}
}
function setupTimeframeMonitor() {
if (timeframeCheckInterval) clearInterval(timeframeCheckInterval);
timeframeCheckInterval = setInterval(function() {
if (chart && chart.timeframe !== previousTimeframe) {
previousTimeframe = chart.timeframe;
loadChart();
}
}, 500);
}
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 = mapTimeframeToFMPParams(chart.timeframe || '1day');
var isIntraday = p.type === 'intraday';
var earliest = new Date(earliestTimestamp);
var toDate = fmtDate(new Date(earliest.getTime() - 86400000));
var fromDate = fmtDate(new Date(earliest.getTime() - p.days * 86400000));
var url = buildFMPUrl(currentTicker, p, apiKey, fromDate, toDate);
var resp = await fetch(url);
var json = await resp.json();
var arr = isIntraday ? json : (json.historical || []);
if (Array.isArray(arr) && arr.length > 0) {
var older = transformFMPData(json, isIntraday);
earliestTimestamp = older[0].t;
chart.prependHistoricalData(older);
} else {
hasMoreHistory = false;
chart.setLoadingHistoricalData(false);
}
} catch(e) {
chart.setLoadingHistoricalData(false);
} finally {
isLoadingMore = false;
}
}
</script>
</body>
</html>
Download the library, grab your FMP API key, and start charting.
See more tutorials: JavaScript Stock Charts Documentation | Twelve Data API Tutorial | Alpha Vantage API Tutorial