Free Stock API With No API Key: curl, Python and JavaScript
A free stock market API with no key or account: the endpoints, curl, Python and JavaScript examples, rate limits, errors, and when you need a keyed provider.
You can pull stock scores, quotes, fundamentals and price history without signing up for anything. This works as-is, with no API key, no account and no header to set:
curl --compressed 'https://api.stocksscores.com/api/analysis/scores?symbol=AAPL'
It returns Apple's 0–100 composite score, the three pillars behind it (technical, fundamental, news), the technical score on every timeframe from 5-minute to quarterly bars, and a quote snapshot, in 13–20 KB of JSON. The StocksScores REST API is free, keyless and has no paid tier; the only limit is per IP address. Below: every endpoint, examples in curl, Python and JavaScript, a short watchlist project, the rate limits, the errors you will actually hit, and an honest look at the other free stock market APIs.
The endpoints
Base URL: https://api.stocksscores.com. Everything is a GET that answers JSON.
| Endpoint | What it returns | Size |
|---|---|---|
/api/analysis/scores?symbol=AAPL | The composite, its three pillars, the technical score per timeframe, the quote | 13–20 KB |
/api/analysis/fundamental?symbol=AAPL | Valuation, growth, profitability, ownership, analyst coverage, and the financial statements themselves | a few hundred KB |
/api/analysis/news?symbol=AAPL | News sentiment, earnings-call transcripts, the headline feed, the SEC filing list | a few hundred KB |
/api/analysis/technical?symbol=AAPL&interval=1d | OHLCV bars and roughly 150 indicator series | 30–400 KB per interval |
/api/analysis/all | The screener feed: last night's score for every covered symbol, with price, sector and the three pillars | 30+ MB raw, 5–6 MB gzipped |
The technical endpoint takes three parameters:
interval: one or more of5m,15m,30m,1h,1d,1wk,1mo,3mo, comma-separated. Anything else is a 400.series:symbol,spyorboth(the default). SPY rides along as a benchmark; ask forsymbolif you only want the stock.fields:ohlcvfor bars only, or a comma-separated list of series names such astimestamp,close,rsi14. Omitted, the interval comes back whole.
Coverage is US-listed common stock above $1 with at least $25M of market cap. Indices, ETFs, funds, OTC names and foreign listings answer HTTP 422 with the reason. Share classes take a hyphen: BRK-B, not BRK.B.
Every score runs 0–100 and higher is more bullish, including metrics where low is good: a cheap P/E scores high. The composite is the technical and fundamental totals at equal weight; news is returned but not counted. A pillar with no data is null and drops out of the composite rather than being counted as zero. The weighting is covered in how 0–100 stock scores work.
What the response looks like
Trimmed, with values from the 7 October 2026 close. They change daily.
{
"symbol": "AAPL",
"quote": { "shortName": "Apple Inc.", "regularMarketPrice": 336.67, "priceToBook": 45.74, ... },
"scores": {
"math": {
"grandTotal": 61.99,
"technical": {
"1d": { "rsi14": 35.07, "sma200": 91.8, ..., "total": 63.79 },
"1wk": { ... }, "5m": { ... }, ...
"total": 67.82
},
"fundamental": { "returnOnEquity": 100, "priceToBookRatio": 0, ..., "total": 56.16 },
"news": { "newsSentiment": 44.16, ..., "total": 48.2 }
}
}
}
The composite is scores.math.grandTotal; each pillar's score is its total. Under technical, each timeframe key holds its own indicator scores and its own total.
One trap: everything under scores.math is a 0–100 score, not a raw measurement. Above, fundamental.priceToBookRatio is 0 because Apple is expensive on book value; the ratio itself, about 45.7, is quote.priceToBook. Likewise technical["1d"].rsi14 is a score, not the RSI reading. For indicator values, use the technical endpoint.
curl
The four headline numbers, via jq:
curl -s --compressed 'https://api.stocksscores.com/api/analysis/scores?symbol=AAPL' \
| jq -c '.scores.math | {composite: .grandTotal, technical: .technical.total,
fundamental: .fundamental.total, news: .news.total}'
{"composite":61.99,"technical":67.82,"fundamental":56.16,"news":48.2}
Daily and weekly bars for the stock alone. Each interval is an object of parallel arrays: timestamp (Unix seconds), open, high, low, close, adjclose, volume.
curl -s --compressed \
'https://api.stocksscores.com/api/analysis/technical?symbol=AAPL&interval=1d,1wk&series=symbol&fields=ohlcv'
Your remaining budget, read from the response headers:
curl -s --compressed -D - -o /dev/null \
'https://api.stocksscores.com/api/analysis/scores?symbol=AAPL' | grep -i '^ratelimit'
Keep --compressed on every call. The screener feed shrinks by a factor of about six.
Python (requests)
No SDK needed. requests asks for gzip by default.
import requests
BASE = "https://api.stocksscores.com/api/analysis"
r = requests.get(f"{BASE}/scores", params={"symbol": "AAPL"}, timeout=60)
r.raise_for_status()
data = r.json()
m = data["scores"]["math"]
print(data["quote"]["shortName"], data["quote"]["regularMarketPrice"])
print("composite ", m["grandTotal"])
print("technical ", m["technical"]["total"], "| daily:", m["technical"]["1d"]["total"])
print("fundamental", m["fundamental"]["total"])
print("news ", m["news"]["total"] if m["news"] else None)
print("requests left:", r.headers["RateLimit-Remaining"])
Apple Inc. 336.67
composite 61.99
technical 67.82 | daily: 63.79
fundamental 56.16
news 48.2
requests left: 974
For indicator values, name the series in fields; bar series and indicators mix freely. Single-line indicators sit under outReal and are shorter than the bars by their warm-up period, so align them from the end:
r = requests.get(
f"{BASE}/technical",
params={"symbol": "AAPL", "interval": "1d", "series": "symbol",
"fields": "timestamp,close,rsi14"},
timeout=60,
)
d = r.json()["1d"]
rsi = d["rsi14"]["outReal"] # no values for the warm-up bars
rsi = [None] * (len(d["close"]) - len(rsi)) + rsi # now rsi[i] lines up with close[i]
print(d["timestamp"][-1], d["close"][-1], rsi[-1])
The arrays drop straight into a pandas DataFrame if you prefer one. Names follow the usual abbreviations (sma50, ema200, macd12, bollingerbands20, atr14, vwap); request an interval without fields once to see the full list. For what each one measures, see essential technical indicators.
JavaScript (fetch in Node)
Node 18 and later ship fetch, which handles gzip for you. Save as .mjs so top-level await works:
// scores.mjs (run with: node scores.mjs)
const BASE = "https://api.stocksscores.com/api/analysis";
const res = await fetch(`${BASE}/scores?symbol=MSFT`);
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const { quote, scores } = await res.json();
const m = scores.math;
console.log(quote.shortName, quote.regularMarketPrice);
console.log({
composite: m.grandTotal,
technical: m.technical.total,
fundamental: m.fundamental.total,
news: m.news?.total ?? null,
});
console.log("requests left:", res.headers.get("ratelimit-remaining"));
Run it from Node, a serverless function, a Worker or a bot, not from a browser page on your own domain. CORS is allowed only for StocksScores' own origins, so a front end has to go through your server.
A small project: rank your watchlist by score
This loops over a watchlist, skips what the API cannot score, and prints the rest best-first. It handles the three failure shapes you will meet: a 422 for an ETF, an empty result for a ticker that does not exist, and a null pillar.
import requests
URL = "https://api.stocksscores.com/api/analysis/scores"
WATCHLIST = ["AAPL", "MSFT", "NVDA", "JPM", "BRK-B", "XOM", "SPY", "NOPE"]
def pillar(m, name):
return (m.get(name) or {}).get("total")
rows = []
for sym in WATCHLIST:
r = requests.get(URL, params={"symbol": sym}, timeout=60)
if r.status_code == 422:
print(f"skip {sym}: {r.json()['error']}")
continue
r.raise_for_status()
m = r.json()["scores"].get("math")
if not m:
print(f"skip {sym}: unknown symbol")
continue
rows.append((m["grandTotal"], sym, pillar(m, "technical"), pillar(m, "fundamental"), pillar(m, "news")))
fmt = lambda x: " - " if x is None else f"{x:5.1f}"
for total, sym, tech, fund, news in sorted(rows, reverse=True):
print(f"{sym:6} {total:5.1f} tech {fmt(tech)} fund {fmt(fund)} news {fmt(news)}")
Output on 7 October 2026 (yours will differ):
skip SPY: SPY is outside the scored universe: not a common stock
skip NOPE: unknown symbol
NVDA 74.2 tech 68.4 fund 80.0 news 46.7
MSFT 65.2 tech 66.6 fund 63.8 news 51.2
XOM 62.9 tech 67.4 fund 58.3 news 37.6
AAPL 62.0 tech 67.8 fund 56.2 news 48.2
BRK-B 59.8 tech 61.3 fund 58.3 news -
JPM 59.4 tech 56.4 fund 62.3 news 45.4
Each composite is the average of its technical and fundamental totals; Berkshire's news total was null that day, which leaves its composite unchanged, because news is not counted.
Past a few dozen symbols, stop looping and download the screener feed once. Each row carries symbol, a short quote (regularMarketPrice, regularMarketChangePercent, marketCap, shortName), scores.math with grandTotal and the pillars, assetProfile.sector and .industry, beta, nextEarningsDate and createdAt. The top ten technology names:
import requests
rows = requests.get("https://api.stocksscores.com/api/analysis/all", timeout=300).json()
tech = [x for x in rows if (x.get("assetProfile") or {}).get("sector") == "Technology"]
tech.sort(key=lambda x: x["scores"]["math"]["grandTotal"], reverse=True)
for x in tech[:10]:
print(x["symbol"], x["scores"]["math"]["grandTotal"], x["quote"]["regularMarketPrice"])
One request replaces thousands, and filtering locally is how a stock screener works anyway. The feed is last night's batch and a per-symbol score may have been recomputed since, so the two can differ slightly.
Rate limits and caching etiquette
- Limit: 1,000 requests per 10 minutes per IP. Responses carry
RateLimit-RemainingandRateLimit-Reset(seconds until the window resets). Over the line is HTTP 429. - Per-symbol scores are recomputed at most every two hours and served
public, max-age=60, s-maxage=300. Polling one symbol every few seconds buys nothing. - The screener feed is written by a nightly batch that starts at 21:30 UTC and takes about four hours. Pull it once a day after that and keep it on disk. Pulled mid-batch, it mixes tonight's rows with last night's; check
createdAt. - The quote is a snapshot from when the score was computed, not a streaming price.
A small wrapper that respects 429s:
import time, requests
def get(path, **params):
while True:
r = requests.get(f"https://api.stocksscores.com{path}", params=params, timeout=60)
if r.status_code != 429:
r.raise_for_status()
return r.json()
time.sleep(int(r.headers.get("RateLimit-Reset", 60)) + 1)
scores = get("/api/analysis/scores", symbol="AAPL")
If real users hit your dashboard, cache on your side and serve them from your cache. Deploying AI stock analysis tools into production goes further.
Errors you'll see
| Response | Cause | Fix |
|---|---|---|
400 {"error":"No symbol provided"} | Missing symbol | Add it |
400 {"error":"Unknown interval: 2d","supported":[...]} | Unsupported interval | Pick one from supported |
422 {"error":"SPY is outside the scored universe: not a common stock","reason":"not-a-stock"} | ETF, index, fund, OTC or foreign listing | Skip it; branch on reason |
| 429 | Over 1,000 requests in 10 minutes | Sleep RateLimit-Reset seconds |
200 with "scores":{} | Unknown ticker or wrong format (BRK.B) | Treat as not found |
200 with a pillar null | No data for that pillar | Handle it; it is not zero |
| CORS error in the browser | Called from a third-party page | Call from a server or script |
The empty-scores case bites hardest: a typo does not 404, so code that only checks the status carries on with nothing.
Other free stock APIs, compared honestly
Most free stock market APIs are free tiers of paid products, and they need a key. We sent a keyless request to Alpha Vantage, Finnhub, Financial Modeling Prep and Massive; all four refused it and asked for one. Free-tier terms below are from each provider's own pages as of October 2026. They change, so check before you build.
| Provider | Key? | Free tier, as of October 2026 |
|---|---|---|
| StocksScores | No | 1,000 requests per 10 minutes per IP; US common stock only |
| Alpha Vantage | Free key | 25 requests per day |
| Finnhub | Free key (token parameter or X-Finnhub-Token header) | A per-minute plan limit (see their pricing page), plus a 30 calls/second ceiling on every plan |
| Financial Modeling Prep | Free key | Basic plan: 250 calls per day, end-of-day historical data |
| Massive (formerly Polygon.io) | Free key | Stocks Basic: 5 calls per minute, end-of-day data, 2 years of history |
yfinance (Python) | No | Unofficial; no published limit |
yfinance is popular because it needs no key, but its README says it is not affiliated with, endorsed or vetted by Yahoo, that it uses Yahoo's publicly available APIs, and that it is intended for research and educational purposes. Nothing about those endpoints is promised to you, so they can change without notice and your code finds out at run time. Fine for a notebook; risky under anything that has to stay up.
You need a keyed, usually paid, provider when you want:
- Real-time or tick-level prices, or deep intraday history.
- ETFs, indices, options, futures, forex, crypto or non-US listings.
- A contract: an SLA, a commercial data licence, redistribution rights.
- More than 1,000 requests per 10 minutes from one address.
For a side project, a dashboard, a notebook or a bot that wants an opinionated read on US stocks, the keyless route covers a lot. The same AAPL data, rendered, is in the StocksScores app.
Using it from an AI assistant
The scores are also an MCP server at https://api.stocksscores.com/mcp, no authentication. Tool calls are metered far more tightly than REST: one a minute and ten a day per IP. Setup for Claude, Cursor and other clients is in the stock market MCP server guide. For bulk work, use REST. The plain-text reference is at app.stocksscores.com/llms.txt.
FAQ
Is there really no API key?
Yes. Every endpoint answers an anonymous request, the limit is counted per IP, and there is no paid tier.
Can I call it from my website's front end?
Not directly; CORS allows only StocksScores' own origins. Put a server route or serverless function in front of it, which also gives you a place to cache.
Does it cover ETFs, indices, crypto or non-US stocks?
No. Those answer 422. SPY does appear as a benchmark series on the technical endpoint, but it is not scored.
Is it a good free stock data API for Python?
It is plain HTTPS and JSON, so requests is all you need, and the price arrays load straight into pandas.
Do I have to credit StocksScores?
Attribution is appreciated, not enforced.
What if 1,000 requests per 10 minutes is not enough?
It usually is once you cache: respect the two-hour recompute, and use the screener feed instead of looping over thousands of symbols.
Scores are information about a stock, not investment advice.