๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Guides

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.

S SparkProxy 3 18 min read
Share
How to Scrape Yahoo Finance Data (2026 Guide)

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.


The Hidden JSON API Behind the Page

Open any ticker on Yahoo Finance, press F12, and watch the Network tab filter to Fetch/XHR. The page paints itself from calls to query1.finance.yahoo.com (and its twin query2.finance.yahoo.com, which load-balances the same routes). Those calls return clean JSON. Scraping that JSON directly is faster than rendering the DOM, costs a fraction of a headless browser, and gives you typed fields instead of text you have to parse out of tags. It is the same reverse-engineering method covered in how to scrape hidden JSON API endpoints, applied to one specific target.

Here is the endpoint map worth pinning to your monitor:

EndpointReturnsCrumb needed?Method
`/v8/finance/chart/{symbol}`Quotes + historical OHLCVNoGET
`/v7/finance/quote?symbols=A,B`Multi-symbol quotesYes (since 2024)GET
`/v10/finance/quoteSummary/{symbol}`Fundamentals, profile, analystsYesGET
`/v1/test/getcrumb`The crumb string itselfCookie onlyGET
`/v7/finance/download/{symbol}`CSV history (retired 2024)BlockedGET

The CSV download route that half the internet still recommends returns 401 Unauthorized now. Do not build on it. The chart endpoint replaced it.

One rule applies to every request: send a real browser User-Agent. The default python-requests/2.x agent gets an instant 429 or an empty body. Yahoo treats a missing or library-default agent as a bot signal before it even looks at your rate.


Free trial

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:

IntervalMax lookbackTypical use
`1m`7 days per request, ~30 days totalIntraday microstructure
`2m` / `5m` / `15m`~60 daysIntraday backtests
`60m` / `1h`~730 daysSwing analysis
`1d`Full history (decades)Daily bars, most common
`1wk` / `1mo`Full historyLong-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:

ModuleKey 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.

ApproachBest forWatch out for
Raw endpointsFull control, custom fields, scaleYou maintain the crumb flow
`yfinance`Fast prototyping, notebooksBreaks on Yahoo changes; single-IP
Licensed API (Polygon, Finnhub)Commercial, real-time, redistributionCosts money; the compliant path

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.

Limited-time ยท 50% off

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

Claim Discount

About the Author

The SparkProxy Technical Team builds and operates SparkProxy's datacenter proxies, residential proxies, and the SparkProxy Scraping API. We work with automated data collection at scale every day, from reverse-engineering private JSON endpoints to keeping high-volume scrapers under rate limits across rotating IP pools. Our guides document what actually holds up in production, including the failure modes and the legal boundaries, not just the happy path.

Keep reading

Related articles

How to Set Up and Use a Proxy in Postman

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.

SparkProxyยทGuides