sifting/io
US Equities
7 min readSiftingIO Team

How to get historical stock price data from a REST API (daily and intraday OHLCV)

How to pull daily and intraday OHLCV stock bars from a REST API: request format, response fields, intervals, cursor pagination, history depth, and pitfalls.

How to get historical stock price data from a REST API (daily and intraday OHLCV)

A historical stock price API returns daily and intraday OHLCV bars (open, high, low, close, volume) as JSON over HTTPS. You send one authenticated GET request with a ticker, an interval, and a date range, and you get back rows you can load into pandas, a database, or a charting library. This guide walks through the exact request shape, the response fields, the intraday intervals, cursor pagination for long ranges, and the pitfalls that cost the most debugging time.

What is the fastest way to get historical stock prices from an API#

Send an authenticated GET request to a historical-bars endpoint with a ticker, an interval, and a date range. On SiftingIO the endpoint is GET /v1/hist/stocks/{ticker}/bars, documented at /docs/historical/stocks. One curl command pulls a week of daily bars for AAPL:

curl --compressed -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/hist/stocks/AAPL/bars?start=2026-05-01&end=2026-05-09&interval=1d"

The --compressed flag matters. It makes curl send Accept-Encoding: gzip, and this endpoint requires gzip. Tickers are case-insensitive, so aapl and AAPL resolve to the same instrument. Coverage spans US-listed common stocks, ETFs, and ADRs; the stocks product page describes the dataset.

What does a historical stock price API request look like#

A request is the ticker in the path plus a handful of query parameters that control the range, the resolution, and the page size.

ParameterDefaultMeaning
startnoneInclusive lower bound, YYYY-MM-DD or RFC 3339
endnowInclusive upper bound, same formats
interval1mBar size: 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1mo
limit1000Bars per page, up to 2000
cursornoneOpaque token from the previous page's meta.next_cursor

Two details are easy to miss. The default interval is 1m, so a request without an explicit interval returns minute bars. And the Accept-Encoding header must include gzip, or the request fails before it returns any data.

What fields come back in the response#

Each bar is five price-and-volume fields plus a timestamp, wrapped in a data array with a meta object for pagination:

{
  "data": [
    {"t": 1746793800000, "o": 198.41, "h": 198.55, "l": 198.36, "c": 198.50, "v": 1234}
  ],
  "meta": {
    "symbol": "AAPL",
    "interval": "1m",
    "as_of": "2026-05-09T20:00:03Z",
    "next_cursor": "eyJvIjoxMDAwfQ"
  }
}
FieldMeaning
tBar start time, UTC epoch in milliseconds
o, h, l, cOpen, high, low, close prices
vVolume
meta.next_cursorToken for the next page, null on the last page

In pandas, pd.to_datetime(df["t"], unit="ms", utc=True) converts the timestamp column correctly. All timestamps are UTC, so there is no exchange-local timezone math to undo.

How do you get intraday bars instead of daily bars#

Set the interval parameter: 1m, 5m, 15m, 30m, and 1h are the intraday resolutions, while 1d, 1w, and 1mo cover daily and above. This request pulls minute bars for a single trading day:

curl --compressed -H "X-API-Key: $SIFTING_KEY" \
  "https://api.sifting.io/v1/hist/stocks/AAPL/bars?start=2026-05-09&end=2026-05-09&interval=1m"

If you need several intraday resolutions, a common pattern is to store 1m bars once and derive the rest locally. How to do that correctly, including which OHLCV fields aggregate with which function, is covered in resampling minute bars into 5m, 15m, and 1h.

How do you pull a long date range without missing rows#

Follow the cursor: request the first page, then pass meta.next_cursor back as the cursor parameter until it comes back null. A page holds at most 2000 bars, and two years of minute data is far more than that, so any long pull is a loop:

import requests

url = "https://api.sifting.io/v1/hist/stocks/NVDA/bars"
params = {"start": "2024-01-01", "end": "2026-01-01", "interval": "1d", "limit": 2000}
headers = {"X-API-Key": SIFTING_KEY}  # requests sends Accept-Encoding: gzip by default

bars = []
while True:
    body = requests.get(url, params=params, headers=headers).json()
    bars.extend(body["data"])
    if body["meta"]["next_cursor"] is None:
        break
    params["cursor"] = body["meta"]["next_cursor"]

The cursor is what makes this loop safe. Offset-based pagination can skip or duplicate rows when the underlying data shifts between requests; an opaque cursor pins your position in the series, so the loop terminates with every bar exactly once.

How far back does historical stock data go#

History depth depends on your plan tier: higher tiers include deeper history, and the pricing page shows what each tier covers. The endpoint accepts any start date, but how far back it actually returns is bounded by your tier's retention window.

Before assuming a long backfill worked, check the earliest t in your results against the date you asked for. A range that starts before your tier's window will simply not reach that far back.

Should you use adjusted or unadjusted prices for a backtest#

Use adjusted prices when computing returns, and unadjusted prices when a number must match what actually printed, such as reconciling a fill or reproducing a historical chart. This endpoint returns as-traded prices and has no adjustment parameter. Adjustment is derived from corporate-actions data, splits and dividends, and applied as a separate step in your own pipeline. The full argument, including how a single split distorts unadjusted returns, is in adjusted vs unadjusted stock prices.

Why not just scrape an undocumented endpoint from a public finance site#

Because an undocumented internal endpoint carries no stability guarantee and can change without notice. Plenty of developers find one in a browser's network tab. It works for a weekend project, then a field gets renamed, the response shape changes, or requests start failing with an opaque block, and there's no changelog or support channel to consult. Rate limits are undeclared, so you learn them by being cut off. A documented API gives you what a scraped endpoint can't: versioned paths, published parameters and limits, cursor pagination, UTC timestamps with a stated convention, and an error format your code can branch on. For a script that runs once, scraping is a shortcut. For anything that runs on a schedule or feeds a model, it's deferred breakage.

Common pitfalls#

Three mistakes account for most of the debugging time on this endpoint.

  1. A 406 gzip_required error means the request didn't offer gzip. Plain curl doesn't send Accept-Encoding by default; add --compressed. Python's requests sends it automatically, but a hand-rolled HTTP client or a proxy that strips headers will hit this.
  2. The t field is epoch milliseconds. Parsing it as seconds puts every bar in January 1970, and pd.to_datetime(df["t"]) without unit="ms" misreads it the same way. Always pass unit="ms" and utc=True.
  3. Omitting interval on a daily pull. The default is 1m, so a one-year request without interval=1d returns hundreds of thousands of minute bars across many pages instead of roughly 250 daily rows.

FAQ#

Does the API provide 1 minute stock bars#

Yes. interval=1m returns minute bars, and it's also the default resolution. Minute-bar timestamps are UTC epoch milliseconds, the same as every other interval.

What securities does the historical bars endpoint cover#

US-listed common stocks, ETFs, and ADRs. Each instrument has a symbol page, for example /symbols/stocks/NVDA, showing what data exists for it.

Are the prices adjusted for splits and dividends#

No. Bars are as-traded prices with no adjustment flag. Apply adjustments yourself from splits and dividends in the corporate-actions data, as described in the backtest section above.

How do you map a ticker to CIK, CUSIP, ISIN, or FIGI#

A ticker is a venue-level label, while regulatory and cross-border datasets key on other identifiers. The company profile endpoint returns the CIK for a ticker, and the full mapping process is covered in mapping a stock ticker to its identifiers.

Is there a free tier for historical stock data#

Yes. The free tier includes 10,000 REST calls per month with no credit card required. That's enough to build and test an integration end to end before committing to a paid plan.

When you're ready to pull your first bars, start building free.

Keep reading

Related posts