API guide
Historical stock data: OHLC bars, adjustments and intraday history by API
A price history looks like a simple table until you ask which close, which session and which time zone. Get those three wrong and a backtest lies to you politely.
On this page
Key takeaways
- Historical stock data comes as OHLCV bars: one open, high, low, close and volume per minute, hour or day, each stamped with the start of its window.
- A US daily bar carries the official open and close with consolidated volume, so its volume can exceed the sum of the minute bars by a quarter or more.
- Intraday US history covers the regular session, 09:30 to 16:00 New York time, with hourly bars anchored at 09:30.
- Daily bars are labelled at midnight UTC of the session date; convert that label to New York time and Friday turns into Thursday evening.
- US bars come back as traded, not split-adjusted: across a 50-for-1 split the price drops fiftyfold overnight until you adjust it yourself.
Historical stock data is the record of a stock's past prices, usually delivered as OHLC bars: one open, high, low and close, plus volume, for each minute, hour or day. You get it from a historical stock data API by naming a symbol, an interval and a date range. On TickerLayer's stock API that is one request: GET /stocks/agg/US:KO/1/day/2026-09-14/2026-09-26 returns ten daily bars for Coca-Cola, oldest first if you add sort=asc.
The request is the easy part. The hard part is knowing exactly what each bar means: which trades it includes, which close it reports, where its window starts, and whether a split two years ago has quietly broken your chart. This guide answers each with real responses captured on 2026-09-28, then gives you a script that downloads clean history to CSV.
What a historical bar contains
Daily bars for US:KO, first page
{
"symbol": "US:KO",
"results_count": 2,
"total_count": 10,4
"results": [
{ "o": 90, "h": 90.36, "l": 88.865, "c": 89.35, "v": 14174159, "t": 1789344000000 },2
{ "o": 88.53, "h": 88.895, "l": 87.804, "c": 88.71, "v": 13195605, "t": 1789430400000 }3
],
"next_offset": 25
}
o / h / l / cFirst, highest, lowest and last price in the window. On US daily bars, o and c are the official open and close.vShares traded in the window. Can be null when no consolidated volume is available.tWindow start, Unix milliseconds UTC. 1789344000000 is 2026-09-14T00:00:00Z, the label for Monday's session.total_countBars in the whole range. results_count is only this page.next_offsetPass it back as offset until it comes back null.
The OHLC explainer shows how those four prices are built from individual trades. This guide is about what they cover, because that is where historical stock prices from two sources stop agreeing.
Intervals, request limits and history depth
| Interval | Path segment | Bars in one US session | Max days per request |
|---|---|---|---|
| 1 minute | 1/minute | 390 | 31 |
| 5 minutes | 5/minute | 78 | 90 |
| 15 minutes | 15/minute | 26 | 180 |
| 1 hour | 1/hour | 7, the last one 30 minutes | 365 |
| 4 hours | 4/hour | 2, at 09:30 and 13:30 | 730 |
| 1 day | 1/day | 1 | 5,000 |
A page holds up to 5,000 bars (limit, default 500), so one month of intraday stock data at one-minute resolution, about 8,200 bars, takes two pages. Longer minute histories are simply more requests, each covering at most 31 days. How far back you can go depends on the plan: Individual plans include 2 years of historical data and Business plans 10. The aggregates reference lists every parameter.
The official close, and where the volume went
A US daily bar is not the sum of its minute bars. Its open and close are the official opening and closing prices, and its volume is consolidated volume for the whole day. The minute bars cover 09:30 to 16:00 New York time and stop there. On an ordinary Friday for Coca-Cola, that difference was large:
Where US:KO volume traded on 2026-09-25
Two things jump out. Volume is U-shaped, heavy at the open and heavy again into the close: the half-hour bar from 15:30 carries 83% of the first full hour's volume in half the time. And 26.6% of the day's volume is in no minute bar at all. That slice is mostly the closing auction and trading outside the regular session, and the daily bar is the only place it shows up.
It matters for three kinds of code. A volume-weighted average built from minute bars will not match one built with daily volume. A backtest that "sells at the close" using the last minute bar uses a different price from the official close: on this day both were 87.81, but nothing guarantees it. And a liquidity filter built on intraday volume understates how much actually traded.
Session boundaries and time zones
Every t is the start of the bar's window in Unix milliseconds, UTC. For intraday bars that is an instant: the first minute bar of 2026-09-25 starts at 1790343000000, which is 13:30 UTC or 09:30 in New York. For daily bars it is a label: midnight UTC on the session date, whenever the market actually traded.
A US trading day on the UTC clock
Hours in UTC
Daylight saving time is the classic bug. The US session opens at 13:30 UTC in summer and 14:30 UTC in winter, and Europe changes its clocks on different weekends, so for a few weeks each year the gap between Frankfurt and New York shrinks by an hour. Store UTC, convert for display, and never hard-code 13:30.
The daily label has its own trap. Convert Friday's t to New York time and you get Thursday at 20:00, because midnight UTC is still the previous evening in the Americas. Treat daily t as a date, not a moment:
import pandas as pd
bars = pd.DataFrame([{"c": 87.81, "t": 1790294400000}]) # the 2026-09-25 daily bar
t = pd.to_datetime(bars["t"], unit="ms", utc=True)
print(t.dt.tz_convert("America/New_York").iloc[0]) # wrong: treats the label as a moment
print(t.dt.date.iloc[0]) # right: the session date2026-09-24 20:00:00-04:00
2026-09-25Other markets follow the same rule with their own calendars. JP:7203 returned two daily bars for the week of 2026-09-21, not five, because Tokyo was closed Monday to Wednesday for public holidays (see Japan market hours). A missing bar on a holiday is correct data, not a gap to fill. The Unix timestamp guide covers the conversions in more depth.
Stock splits: raw versus adjusted prices
What is a stock split? A company divides each share into several, so the price per share falls in proportion and the share count rises, while the company's value does not change. In a 50-for-1 split, one share at $3,283 becomes fifty shares at about $65.66.
Historical bars can be served two ways. Adjusted history rewrites every bar before the split so the chart is continuous. Raw history keeps the prices that actually printed. TickerLayer's US bars come back raw, which you can see across a real 50-for-1 split in June 2024:
| Session | Open | Close | Volume | Adjusted close |
|---|---|---|---|---|
| 2024-06-24 | 3,214.75 | 3,193.74 | 412,931 | 63.87 |
| 2024-06-25 | 3,216.30 | 3,283.04 | 481,061 | 65.66 |
| 2024-06-26 | 65.81 | 65.86 | 27,266,346 | 65.86 |
| 2024-06-27 | 65.875 | 62.41 | 28,454,247 | 62.41 |
| 2024-06-28 | 62.03 | 62.65 | 22,115,119 | 62.65 |
Unadjusted, the chart shows a 98% crash on 2024-06-26 that never happened, and every moving average, return and volatility figure that crosses the date is wrong. The fix is arithmetic on the bars dated before the split's effective date:
adjusted price = raw price ÷ split ratioadjusted volume = raw volume × split ratio
- split ratio
- New shares per old share: 50 for a 50-for-1 split, 0.125 for a 1-for-8 reverse split.
- raw price
- Any open, high, low or close dated before the effective date.
Raw bars are right for
- Reproducing the prices that actually printed
- Simulating orders and fills at real price levels
- Reconciling against statements and trade confirmations
Adjusted bars are right for
- Returns and moving averages that cross a split
- Long-range charts
- Volatility and correlation studies
Dividends are the other adjustment. A total-return series also scales earlier prices down by each cash dividend, so a stock that pays out does not appear to fall on its ex-dividend date. Raw bars do not do that either. Keep raw bars as the source of truth, store splits and dividends in their own table, and apply adjustments when you read, so you can always reproduce the prices that actually printed.
Download historical stock data to CSV
- Date rangefrom and to, UTC
- Chunksat most 31 days of minutes
- Pagesfollow next_offset
- CSV rowsone bar per row
- Adjust on readsplits, dividends
This exporter splits a long date range into chunks the API accepts, follows next_offset through every page, retries a per-second 429 after its Retry-After, and writes one row per bar with the symbol and a UTC timestamp. It needs Python 3.9 or newer and pip install requests:
import csv
import os
import time
from datetime import date, datetime, timedelta, timezone
import requests
BASE_URL = "https://api.tickerlayer.com"
# Longest date range one request may cover, per interval (see the REST docs).
MAX_DAYS = {"1/minute": 31, "5/minute": 90, "15/minute": 180, "1/hour": 365, "4/hour": 730, "1/day": 5000}
session = requests.Session()
session.headers.update({"x-api-key": os.environ["TICKERLAYER_API_KEY"]})
def get(path, params):
while True:
resp = session.get(BASE_URL + path, params=params, timeout=15)
if resp.status_code == 429 and "Retry-After" in resp.headers:
time.sleep(float(resp.headers["Retry-After"])) # per-second limit
continue
resp.raise_for_status() # 400 bad range, 403 plan, 404 symbol, quota 429
return resp.json()
def windows(start, end, max_days):
"""Split an inclusive date range into chunks the API accepts."""
while start <= end:
stop = min(start + timedelta(days=max_days - 1), end)
yield start, stop
start = stop + timedelta(days=1)
def fetch_bars(symbol, interval, start, end):
multiplier, timespan = interval.split("/")
for a, b in windows(start, end, MAX_DAYS[interval]):
path = f"/stocks/agg/{symbol}/{multiplier}/{timespan}/{a}/{b}"
params = {"sort": "asc", "limit": 5000, "offset": 0}
while True:
body = get(path, params)
yield from body["results"]
if body["next_offset"] is None:
break
params["offset"] = body["next_offset"]
def export_csv(symbols, interval, start, end, filename):
with open(filename, "w", newline="") as f:
out = csv.writer(f)
out.writerow(["symbol", "bar_start_utc", "open", "high", "low", "close", "volume"])
for symbol in symbols:
count = 0
for bar in fetch_bars(symbol, interval, start, end):
start_utc = datetime.fromtimestamp(bar["t"] / 1000, tz=timezone.utc)
out.writerow([symbol, start_utc.isoformat(), bar["o"], bar["h"], bar["l"], bar["c"], bar["v"]])
count += 1
print(f"{symbol}: {count} bars")
export_csv(["US:KO", "US:JPM"], "1/day", date(2026, 9, 1), date(2026, 9, 25), "daily_bars.csv")US:KO: 18 bars
US:JPM: 18 bars
symbol,bar_start_utc,open,high,low,close,volume
US:KO,2026-09-01T00:00:00+00:00,89.59,89.78,87.81,88,14920372
US:KO,2026-09-02T00:00:00+00:00,87.895,88.95,87.73,88.24,18587734
US:KO,2026-09-03T00:00:00+00:00,88.11,89.225,88.01,88.81,13857015Eighteen bars, not nineteen weekdays: the US market was closed for Labor Day on 2026-09-07. Switch to "1/minute" and a full quarter, and the same script walks three 31-day windows with two pages each. Run it after the close, or drop the last intraday bar: during market hours the bar for the session in progress is included and still changing. For the same data in a pandas DataFrame, see the Python stock prices tutorial.
Before you trust a backtest
- Daily questions use daily bars, with the official close.
- Splits are adjusted, or the date range avoids them, and you know which.
- Timestamps stay in UTC until display, and daily
tis treated as a date. - The newest intraday bar is dropped or refreshed if it was fetched mid-session.
- Holiday gaps stay gaps; nothing fills them with invented bars.
- Volume comparisons use one definition: minute sums or daily totals, never a mix.
- Symbols are market-qualified, so a ticker that exists in two markets cannot swap histories.
- The stock universe is chosen as of each test date, not from today's list (survivorship bias).
Questions
How do I download historical stock data?
Request bars from a historical stock data API and write them to a file. With TickerLayer, GET /stocks/agg/{symbol}/1/day/{from}/{to} returns up to 5,000 daily bars per page, and the Python exporter in this guide writes them to CSV.
Is historical stock data adjusted for splits?
It depends on the source. TickerLayer's US bars come back as traded, so a 50-for-1 split appears as a fiftyfold price drop until you divide earlier prices by the split ratio.
What is the difference between the close and the adjusted close?
The close is the price the stock actually finished at that day. The adjusted close rescales earlier prices for splits and, in total-return series, dividends, so returns across those events are continuous.
Does intraday stock data include pre-market and after-hours trading?
TickerLayer's REST intraday bars for US stocks cover the regular session, 09:30 to 16:00 New York time. Pre-market and after-hours bars are available live on the stocks.agg WebSocket channel with session: "extended".
How far back does historical stock data go?
On TickerLayer, Individual plans include 2 years of historical data and Business plans 10 years. One request covers at most 31 days of minute bars or 5,000 days of daily bars.
Why is daily volume higher than the sum of intraday volume?
The daily bar reports consolidated volume for the whole day, including the closing auction and trading outside the regular session, while intraday bars cover 09:30 to 16:00 only. For US:KO on 2026-09-25 the gap was 3.26 million shares.