The journal
Entry No.46October 8, 2026 · tom

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.

EndpointWhat it returnsSize
/api/analysis/scores?symbol=AAPLThe composite, its three pillars, the technical score per timeframe, the quote13–20 KB
/api/analysis/fundamental?symbol=AAPLValuation, growth, profitability, ownership, analyst coverage, and the financial statements themselvesa few hundred KB
/api/analysis/news?symbol=AAPLNews sentiment, earnings-call transcripts, the headline feed, the SEC filing lista few hundred KB
/api/analysis/technical?symbol=AAPL&interval=1dOHLCV bars and roughly 150 indicator series30–400 KB per interval
/api/analysis/allThe screener feed: last night's score for every covered symbol, with price, sector and the three pillars30+ MB raw, 5–6 MB gzipped

The technical endpoint takes three parameters:

  • interval: one or more of 5m, 15m, 30m, 1h, 1d, 1wk, 1mo, 3mo, comma-separated. Anything else is a 400.
  • series: symbol, spy or both (the default). SPY rides along as a benchmark; ask for symbol if you only want the stock.
  • fields: ohlcv for bars only, or a comma-separated list of series names such as timestamp,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-Remaining and RateLimit-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

ResponseCauseFix
400 {"error":"No symbol provided"}Missing symbolAdd it
400 {"error":"Unknown interval: 2d","supported":[...]}Unsupported intervalPick 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 listingSkip it; branch on reason
429Over 1,000 requests in 10 minutesSleep RateLimit-Reset seconds
200 with "scores":{}Unknown ticker or wrong format (BRK.B)Treat as not found
200 with a pillar nullNo data for that pillarHandle it; it is not zero
CORS error in the browserCalled from a third-party pageCall 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.

ProviderKey?Free tier, as of October 2026
StocksScoresNo1,000 requests per 10 minutes per IP; US common stock only
Alpha VantageFree key25 requests per day
FinnhubFree 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 PrepFree keyBasic plan: 250 calls per day, end-of-day historical data
Massive (formerly Polygon.io)Free keyStocks Basic: 5 calls per minute, end-of-day data, 2 years of history
yfinance (Python)NoUnofficial; 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.