How to Scrape Yahoo Finance Data (2026 Guide)
Scrape Yahoo Finance quotes, historical prices, and fundamentals from its hidden JSON API. Crumb and cookie setup, 429 fixes, Python code, and the legal rules.

You can scrape Yahoo Finance without rendering a single line of HTML, because every number on the page already arrives as JSON from a handful of undocumented endpoints on query1.finance.yahoo.com. The trick most beginners miss is that two of those endpoints stopped answering in 2024 unless you send a matching crumb and cookie, and a third one throttles you with a 429 long before your script finishes. This guide maps each endpoint to the data it returns, walks through the crumb and cookie handshake step by step, shows the Python that survives the rate limits, and covers the terms of service you actually have to respect.
What You Can Scrape from Yahoo Finance
Yahoo Finance is the default free source for retail equity data, and the data splits into three buckets that behave very differently once you automate them:
- Quotes: last price, day change, volume, bid and ask, market cap, and the intraday range you see at the top of a ticker page.
- Historical prices: daily, weekly, or monthly open/high/low/close/volume (OHLCV) bars, plus adjusted close, dividends, and split events going back decades.
- Fundamentals: revenue, EPS, P/E, margins, balance-sheet lines, cash flow, analyst targets, and the company profile.
Each bucket has its own endpoint, its own auth rule, and its own gotcha. Quotes and historical prices come from the same chart endpoint and need no authentication. Fundamentals and the multi-symbol quote endpoint both demand a crumb token that did not exist before Yahoo tightened access in 2024. If you only remember one thing from this section, remember that "public page" does not mean "open API."
For the broader picture across Alpha Vantage, SEC EDGAR, and other sources, see how to scrape stock market data. This post stays narrow and goes deep on Yahoo specifically.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Endpoint 1: Quotes (Live and Delayed)
The chart endpoint carries the current quote inside its meta block, so you get a quote for free every time you ask for a chart. No crumb required.
curl -s "https://query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1d&interval=1d" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
The meta object holds the numbers most people want:
import requests
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
def get_quote(symbol):
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
r = requests.get(url, params={"range": "1d", "interval": "1d"},
headers={"User-Agent": UA}, timeout=15)
r.raise_for_status()
m = r.json()["chart"]["result"][0]["meta"]
return {
"symbol": m["symbol"],
"price": m["regularMarketPrice"],
"previous_close": m.get("chartPreviousClose"),
"currency": m["currency"],
"exchange": m["exchangeName"],
"market_time": m["regularMarketTime"],
}
print(get_quote("AAPL"))
One caveat that matters for anyone building a trading tool: the free quote is usually delayed. For most US-listed names Yahoo serves a 15-minute-delayed price, and the meta block tells you so through fields like regularMarketTime. Real-time ticks are licensed by the exchanges (Nasdaq, NYSE), and Yahoo does not hand those to unauthenticated scrapers. Treat the number as "recent," not "live," unless you have paid for real-time entitlement somewhere.
If you need several tickers at once, /v7/finance/quote?symbols=AAPL,MSFT,TSLA returns them in a single call, but that route now needs a crumb. Jump to the crumb section before you use it.
Endpoint 2: Historical Prices (OHLCV)
Historical bars also come from the chart endpoint. You control the window with either a named range or an explicit period1/period2 pair in unix seconds, and you set the bar size with interval.
# One year of daily bars, with dividends and splits
curl -s "https://query1.finance.yahoo.com/v8/finance/chart/AAPL?period1=1754784000&period2=1786320000&interval=1d&events=div%2Csplit" \
-H "User-Agent: Mozilla/5.0 ... Chrome/126.0.0.0 Safari/537.36"
period1 and period2 are unix timestamps in seconds, not milliseconds. Generate them in code rather than hand-counting:
from datetime import datetime, timezone
import requests
def history(symbol, start, end, interval="1d"):
p1 = int(datetime(*start, tzinfo=timezone.utc).timestamp())
p2 = int(datetime(*end, tzinfo=timezone.utc).timestamp())
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
r = requests.get(url, headers={"User-Agent": UA}, timeout=20, params={
"period1": p1, "period2": p2,
"interval": interval, "events": "div,split",
})
r.raise_for_status()
res = r.json()["chart"]["result"][0]
ts = res["timestamp"]
q = res["indicators"]["quote"][0]
adj = res["indicators"].get("adjclose", [{}])[0].get("adjclose")
rows = []
for i, t in enumerate(ts):
rows.append({
"date": datetime.fromtimestamp(t, timezone.utc).date().isoformat(),
"open": q["open"][i], "high": q["high"][i],
"low": q["low"][i], "close": q["close"][i],
"adj_close": adj[i] if adj else None,
"volume": q["volume"][i],
})
return rows
bars = history("AAPL", (2025, 8, 10), (2026, 8, 10))
print(len(bars), "bars", bars[-1])
The interval field decides granularity, and Yahoo caps how far back each granularity reaches. These retention limits are the second thing that trips people up, right after the crumb:
| Interval | Max lookback | Typical use |
|---|---|---|
| `1m` | 7 days per request, ~30 days total | Intraday microstructure |
| `2m` / `5m` / `15m` | ~60 days | Intraday backtests |
| `60m` / `1h` | ~730 days | Swing analysis |
| `1d` | Full history (decades) | Daily bars, most common |
| `1wk` / `1mo` | Full history | Long-horizon studies |
Ask for 1m bars going back a year and you get an empty array, not an error. Watch the retention column, and page intraday requests in one-week or two-week slices.
Always prefer adj_close over close for any return calculation. The raw close is not split-adjusted or dividend-adjusted, so a stock that split will show a fake overnight drop in the close column that the adj_close column smooths out.
Endpoint 3: Fundamentals with quoteSummary
Fundamentals live at /v10/finance/quoteSummary/{symbol}, and you pull them by naming modules. Each module is a different slice of the company. This endpoint requires the crumb (covered next), so read this section for the shape of the data, then wire in the auth from the following section.
import requests
MODULES = ",".join([
"price", "summaryDetail", "defaultKeyStatistics",
"financialData", "assetProfile", "earnings",
])
def fundamentals(symbol, crumb, cookie):
url = f"https://query1.finance.yahoo.com/v10/finance/quoteSummary/{symbol}"
r = requests.get(url, timeout=20,
headers={"User-Agent": UA, "Cookie": cookie},
params={"modules": MODULES, "crumb": crumb})
r.raise_for_status()
return r.json()["quoteSummary"]["result"][0]
# data["summaryDetail"]["marketCap"]["raw"]
# data["defaultKeyStatistics"]["trailingEps"]["raw"]
# data["financialData"]["profitMargins"]["raw"]
# data["assetProfile"]["sector"], data["assetProfile"]["industry"]
Most numeric fields arrive as an object with three keys: raw (the number), fmt (a display string like "2.94T"), and longFmt. Read raw for math, fmt for display. Here are the modules worth knowing:
| Module | Key fields |
|---|---|
| `price` | `regularMarketPrice`, `marketCap`, `currency` |
| `summaryDetail` | `trailingPE`, `dividendYield`, `fiftyTwoWeekHigh`, `volume` |
| `defaultKeyStatistics` | `trailingEps`, `forwardPE`, `beta`, `sharesOutstanding` |
| `financialData` | `currentPrice`, `targetMeanPrice`, `recommendationKey`, `profitMargins`, `totalRevenue` |
| `assetProfile` | `sector`, `industry`, `fullTimeEmployees`, `longBusinessSummary` |
| `incomeStatementHistory` | Annual revenue, gross profit, net income |
| `balanceSheetHistory` | Assets, liabilities, cash |
| `cashflowStatementHistory` | Operating, investing, financing cash flow |
Request only the modules you use. A quoteSummary call asking for eight modules is heavier and slower than one asking for two, and Yahoo counts every one against your rate budget.
Handling 429 Throttling and Rate Limits
Yahoo does not publish a rate limit, and that is the point. The limit is unofficial, per-IP, and it moves. In practice sustained scraping from a single IP starts drawing 429 Too Many Requests after a few hundred requests, and a burst of rapid calls trips it far sooner. Once you are throttled, the same IP stays cold for minutes.
Two habits keep a single-IP scraper alive: back off on every 429, and pace your requests so you never sprint.
import time, requests
def fetch_json(session, url, params, tries=5):
delay = 1.0
for attempt in range(tries):
r = session.get(url, params=params, timeout=20)
if r.status_code == 429:
time.sleep(delay)
delay *= 2 # exponential backoff: 1s, 2s, 4s, 8s...
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"Throttled after {tries} attempts: {url}")
Backoff buys you time on one IP, but it does not raise the ceiling. If you need thousands of tickers or a full historical backfill, one IP is the wrong tool no matter how politely it waits. The rate limit is per-IP, so the real fix is to spread requests across many IPs. That is a rotation problem, and the same pattern from how to rotate proxies in Python applies here.
Scrape Yahoo Finance at Scale with SparkProxy
The SparkProxy Scraping API turns the per-IP rate limit into a non-problem. You send one request to https://scrape.sparkproxy.io/api/v1, authenticate with the X-API-Key header, and SparkProxy fetches the target from a rotating IP for you. Because the Yahoo endpoints already return JSON, you keep render_js=false and pay 1 credit per call instead of the 5 a headless browser would cost.
curl -s "https://scrape.sparkproxy.io/api/v1" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "url=https://query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1mo&interval=1d" \
--data-urlencode "render_js=false" \
--data-urlencode "country_code=US" \
-G
country_code=US does double duty here: it hands you a US exit IP, which is what dodges the European consent wall from the crumb section. In Python:
import requests, json
API = "https://scrape.sparkproxy.io/api/v1"
KEY = "YOUR_API_KEY"
UA = "Mozilla/5.0 ... Chrome/126.0.0.0 Safari/537.36"
def spark_chart(symbol):
target = (f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
f"?range=1mo&interval=1d")
r = requests.get(API, headers={"X-API-Key": KEY}, timeout=60, params={
"url": target,
"render_js": "false",
"country_code": "US",
"forward_headers": json.dumps({"User-Agent": UA}),
})
r.raise_for_status()
return r.json()["chart"]["result"][0]["meta"]["regularMarketPrice"]
print(spark_chart("MSFT"))
Here is the insight that most Yahoo scraping write-ups miss. The crumb is bound to the cookie, not to the IP address. So you mint one crumb and cookie pair a single time, then fan every subsequent request out across SparkProxy's rotating pool while carrying that same pair in forward_headers. Each request lands on a fresh IP, spreading your call volume across the rate-limit budgets of dozens of addresses, while Yahoo sees one consistent, valid session.
import requests, json
def spark_fundamentals(symbol, crumb, cookie):
target = (f"https://query1.finance.yahoo.com/v10/finance/quoteSummary/"
f"{symbol}?modules=summaryDetail,financialData&crumb={crumb}")
r = requests.get(API, headers={"X-API-Key": KEY}, timeout=60, params={
"url": target,
"render_js": "false",
"country_code": "US",
# reuse ONE crumb+cookie across every rotating IP
"forward_headers": json.dumps({"User-Agent": UA, "Cookie": cookie}),
})
r.raise_for_status()
return r.json()["quoteSummary"]["result"][0]
Decoupling the session (the crumb) from the exit IP (the rate-limit budget) is what makes a full-market backfill practical without a 429 in sight. For teams whose needs go past raw ticks into alternative and market-intelligence feeds, using proxies for financial data collection frames the same infrastructure at a program level.
The Shortcut: yfinance
If you would rather not maintain the crumb handshake yourself, the open-source yfinance library wraps all of it: cookies, crumb, consent flow, and the endpoint quirks.
import yfinance as yf
t = yf.Ticker("AAPL")
print(t.fast_info["last_price"]) # quote
hist = t.history(period="1y", interval="1d") # OHLCV DataFrame
print(hist.tail())
print(t.income_stmt) # fundamentals
print(t.balance_sheet)
One honest warning: yfinance breaks whenever Yahoo changes the private API, which is exactly what happened when the crumb requirement landed in 2024. Pin nothing and update often (pip install -U yfinance), and keep the raw-endpoint knowledge from this guide as your fallback for the week after every Yahoo change, when the library is patched but not yet released. Under real load, yfinance still runs from your one IP, so route it through a proxy or the Scraping API for anything past a handful of symbols.
| Approach | Best for | Watch out for |
|---|---|---|
| Raw endpoints | Full control, custom fields, scale | You maintain the crumb flow |
| `yfinance` | Fast prototyping, notebooks | Breaks on Yahoo changes; single-IP |
| Licensed API (Polygon, Finnhub) | Commercial, real-time, redistribution | Costs money; the compliant path |
Is Scraping Yahoo Finance Legal?
Scraping public data is not automatically fine, and financial data carries sharper edges than most. Three things to hold in your head:
First, Yahoo's Terms of Service restrict automated access and reserve the data for personal, non-commercial use. Yahoo retired its official public finance API back in 2017, so the JSON endpoints you are calling are undocumented and unsupported, which means they can change or close without notice and using them runs against Yahoo's stated terms. That is a business-risk decision, not a settled legal green light.
Second, the data itself is licensed. Real-time exchange quotes belong to the exchanges that produce them (Nasdaq, NYSE and others), which is why Yahoo shows most retail users a 15-minute delay. Redistributing Yahoo quotes, or building a commercial product on top of them, can pull you into exchange licensing terms you never signed.
Third, the compliant path for commercial use is a real API. If you are shipping a product, price out Polygon.io, Finnhub, Alpha Vantage, or Nasdaq Data Link. They cost money and they remove the legal and technical fragility in one move. Scraping Yahoo is a fine way to learn, prototype, or run personal analysis. It is a shaky foundation for a business, and no proxy changes that. Scrape responsibly: identify a real User-Agent, respect the rate limits instead of hammering, cache aggressively, and never present delayed data as real-time.
Frequently asked questions
FAQ
No. Yahoo does not issue API keys for these endpoints; the official public API was retired in 2017. The /v8/finance/chart route works with just a browser User-Agent, while quoteSummary and /v7/finance/quote need a crumb and cookie pair you mint yourself, not a key.
Because quoteSummary and the v7 quote endpoint require a crumb token that matches your session cookie. If the crumb is missing, expired, or was minted against a different cookie, Yahoo returns 401. Fetch the cookie and the crumb together in one requests.Session and the error goes away.
Call /v8/finance/chart/{symbol} with period1 and period2 as unix seconds and interval=1d. The response holds a timestamp array plus open, high, low, close, adjusted close, and volume arrays. Use the adjusted close for any return math so splits and dividends are handled.
Yahoo enforces an unpublished per-IP rate limit. A default library User-Agent triggers it instantly, and sustained requests from one IP trip it after a few hundred calls. Fix it by sending a real User-Agent, adding exponential backoff, and rotating IPs through a proxy or the SparkProxy Scraping API for volume.
Yes, and for prototyping it is the fastest path because it handles the cookie, crumb, and consent flow for you. Keep it updated, since it breaks whenever Yahoo changes the private API, and route it through a proxy for anything beyond a few symbols because it still runs from a single IP.
Yahoo's terms reserve the data for personal, non-commercial use, and real-time exchange quotes are separately licensed by the exchanges. For a commercial product, use a licensed provider such as Polygon, Finnhub, or Nasdaq Data Link. Scraping Yahoo is reasonable for learning, prototyping, and personal analysis, not as a business foundation.
Get 50% off your first purchase
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon โ claim it before it's gone
Related articles
How to Scrape Redfin Data: Listings, Prices, Market
Learn how to scrape Redfin data: pull listings, prices, and property details from Redfin's Stingray JSON API and CSV export, and handle its rate limits.

How to Scrape IMDb Data: Ratings, Cast, Reviews
Learn how to scrape IMDb data: titles, ratings, cast, and reviews. Pull IMDb's JSON-LD and hidden JSON, then use the official datasets for bulk facts.

How to Set Up and Use a Proxy in Postman
Set up a proxy in Postman the right way: custom proxy host and port, proxy auth, SSL cert fixes, verify the exit IP in the Console, plus Newman env vars.
