---
title: "1.3.3 Retail and prosumer market data APIs"
description: "Massive (formerly Polygon.io), Alpaca, Tiingo, EODHD, Alpha Vantage, Twelve Data and FMP: plans, limits and a REST client with pagination and 429 errors."
url: "https://pythonparatrading.com/en/python-trading-course/level-1/retail-and-prosumer-market-data-apis/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-1/retail-and-prosumer-market-data-apis.png"
---

# 1.3.3 · Retail and prosumer market data APIs

> Between free sources and institutional providers there is a tier of APIs with plans costing tens of dollars a month. This lesson compares seven using their own pages, reads the format of a real response and writes a client that paginates and respects the request limit against a local server.

- Course: [Level 1: Financial data, markets and analysis](https://pythonparatrading.com/en/python-trading-course/level-1/)
- Module: 1.3 · Market data sources
- Duration: 55 min
- Prerequisites: [1.3.1 · Types of market data: EOD, bars, ticks, order book and reference](https://pythonparatrading.com/en/python-trading-course/level-1/types-of-market-data/), [1.3.2 · Free market data sources](https://pythonparatrading.com/en/python-trading-course/level-1/free-market-data-sources/)

## By the end of this lesson

- Place Massive (formerly Polygon.io), Alpaca, Tiingo, EODHD, Alpha Vantage, Twelve Data and Financial Modeling Prep by what each plan provides: history, delay, limits and price.
- Convert the limits of the free plans to the same unit and estimate how long a historical download takes.
- Read the JSON response of a bars endpoint (timestamps in milliseconds, `results`, `next_url`) and turn it into a DataFrame.
- Write a REST client that follows `next_url`, reacts to a 429 with `Retry-After` and does not leave the key in the code.

## The tier between free and institutional

The sources in lesson 1.3.2 are free, but either unofficial (yfinance) or they do not cover the market in detail
(FRED, EDGAR, central banks). The providers in lesson 1.3.4 cover everything, at institutional prices. In between
there is a group of companies that sell data APIs to individuals, developers and small firms: the **retail** and
**prosumer** market. They have three things in common:

- A free plan, with short history or few requests, for trying them out.
- Paid plans of tens of dollars a month, which extend history, remove limits and reduce the delay.
- A documented REST API, sometimes websockets for real time, and an official Python client.

This lesson compares seven using what their own pages say. All the prices and limits that follow were read on each
provider's page **as of October 21, 2026**. They change often: before paying you have to read them again.

## Massive, formerly Polygon.io

Polygon.io changed its name to **Massive** on October 30, 2025. According to the announcement, the old API
(`api.polygon.io`) keeps working “for an extended period” in parallel with the new one (`api.massive.com`), and the
old pages redirect to the new ones[^1]. The official Python client is now published as `massive`; the old package,
`polygon-api-client`, is still on PyPI but its latest version is from the day of the name change.

Its stocks plans are a clear ladder:

| Plan | Price per month | History | Delay | Requests |
|---|---|---|---|---|
| Stocks Basic | 0 USD | 2 years | End of day | 5 per minute |
| Stocks Starter | 29 USD | 5 years | 15 minutes | Unlimited |
| Stocks Developer | 79 USD | 10 years | 15 minutes | Unlimited; adds trades |
| Stocks Advanced | 199 USD | 20+ years | Real time | Unlimited; adds quotes |

The figure in the header summarizes the four plans. Prices with monthly billing; the page offers a 20% discount with
the annual plan. Real time is only in Advanced, and the page marks it as for non-professional users (“Non-pros
only”): the difference between professional and non-professional is the one in the exchanges' licenses (lesson
1.3.7). What the plans add is not just history: Developer adds trades (tick by tick) and Advanced, quotes (bid and
ask), which are the data of lessons 1.1.3 and 1.3.1.

## The rest, plan by plan

| Provider | Free plan | Cheapest paid plan with US stocks | Detail |
|---|---|---|---|
| Alpaca | Basic: real time from IEX only, history since 2016 without the last 15 minutes, 200 calls per minute | Algo Trader Plus: 99 USD a month, all US exchanges, 10,000 calls per minute | The broker provides the data with the account; pagination with `next_page_token` |
| Tiingo | Starter: 50 requests per hour, 1,000 a day, 500 symbols a month | Power: 30 USD a month (300 a year) for individuals; 50 USD for internal commercial use | End of day with more than 30 years, with adjusted prices, dividends and splits in the same response |
| EODHD | 20 calls a day, 1 year of history | EOD Historical All-World: 19.99 USD a month | Charges each call in units: 1 for end of day, 5 for intraday, 10 for fundamentals |
| Alpha Vantage | 25 requests a day | 49.99 USD a month: 75 requests per minute and US data with a 15-minute delay | Real-time and delayed data are paid |
| Twelve Data | Basic: 8 credits per minute, 800 a day | Grow: from 29 USD a month (55 credits per minute) | Billed annually, cheaper |
| Financial Modeling Prep | Basic: 250 calls a day, 5 years of end of day | Starter: 29 USD a month (19 USD a month with annual billing), 300 per minute | Strong in fundamentals |

Sources: each provider's pricing or plans page, read on October 21, 2026[^2]. The plans are not equivalent: some are
end of day, others delayed intraday, others real time from a single exchange. Comparing only the price would be
comparing different things.

*Fig. — Price of each provider's cheapest paid plan with US stocks, billed monthly, as of 21/10/2026. The plans are not equivalent: see the table.* Horizontal bars with the monthly price: EODHD end of day 19.99 dollars; Massive Starter 29, highlighted; Twelve Data Grow 29; FMP Starter 29; Tiingo Power 30; Alpha Vantage 49.99; Alpaca Algo Trader Plus 99.

Two details that do not show in the table and that matter:

- **Which exchange provides the “real time”.** Alpaca's free plan provides real time from IEX only, one of the US
  exchanges: the price is real, but the volume is a fraction of the total and the best price may not be that of the
  consolidated market (lesson 1.1.1). For the consolidated data you need the paid plan.
- **Credits, not requests.** EODHD and Twelve Data count credits, and not all calls cost the same. A limit of
  “100,000 calls a day” runs out sooner if each intraday call costs five.

## Comparing limits in the same unit

The free plans give their limits in different units. To compare them you have to convert them to a single one, for
example requests per day, knowing that it is a theoretical ceiling: nobody makes requests nonstop for 24 hours.

```python
import math

import pandas as pd

limites = pd.DataFrame([
    ("Massive Basic", 5, "minute"),
    ("Alpaca Basic", 200, "minute"),
    ("Tiingo Starter", 1000, "day"),
    ("Twelve Data Basic", 800, "day"),
    ("FMP Basic", 250, "day"),
    ("Alpha Vantage", 25, "day"),
    ("EODHD free", 20, "day"),
], columns=["plan", "limite", "por"]).set_index("plan")

por_dia = {"minute": 24 * 60, "hour": 24, "day": 1}
limites["al_dia"] = limites["limite"] * limites["por"].map(por_dia)
limites.sort_values("al_dia", ascending=False)
```
```output
                   limite     por  al_dia
plan
Alpaca Basic          200  minute  288000
Massive Basic           5  minute    7200
Tiingo Starter       1000     day    1000
Twelve Data Basic     800     day     800
FMP Basic             250     day     250
Alpha Vantage          25     day      25
EODHD free             20     day      20
```

Tiingo has two limits, one per hour (50) and one per day (1,000); here the more restrictive of the two counts, the
daily one, because 50 per hour for 24 hours would be 1,200. Twelve Data also has two (8 per minute and 800 a day).
With this ceiling you can already estimate how long the download that matters takes:

```python
acciones, por_peticion = 500, 1   # one request per stock
peticiones = acciones * por_peticion
limites["dias"] = (peticiones / limites["al_dia"]).apply(math.ceil)
limites[["al_dia", "dias"]].sort_values("dias")
```
```output
                   al_dia  dias
plan
Massive Basic        7200     1
Alpaca Basic       288000     1
Tiingo Starter       1000     1
Twelve Data Basic     800     1
FMP Basic             250     2
Alpha Vantage          25    20
EODHD free             20    25
```

*Fig. — Free-plan limits converted to requests per day (theoretical ceiling), as of 21/10/2026.* Log-scale dot chart with the requests per day of each free plan: Alpaca Basic 288,000 (200 per minute), Massive Basic 7,200 (5 per minute), Tiingo 1,000, Twelve Data 800, FMP 250, Alpha Vantage 25, highlighted, and EODHD 20. There are four orders of magnitude from first to last.

With Massive, Alpaca, Tiingo or Twelve Data, the daily history of 500 stocks fits in one day (with Massive, at 5 per
minute, it is 100 minutes); with FMP, in two. With Alpha Vantage or EODHD you need 20 or 25 days. That does not make
these plans useless: it forces you to design the download so that it stores what it has fetched and continues the
next day without repeating (lesson 1.4.8). And there is the other half of the problem: the Massive Basic plan only
has 2 years of history, and EODHD's free plan, 1. The limit tells you how long it takes; the history, whether what
you need exists in that plan.

## Anatomy of a bars response

The Massive bars endpoint is `/v2/aggs/ticker/{ticker}/range/{multiplier}/{timespan}/{from}/{to}`. The response
carries metadata (`status`, `request_id`, `resultsCount`, `adjusted`), the `results` list with one bar per element
and, if there are more pages, `next_url`. Each bar has `o`, `h`, `l`, `c` (prices), `v` (volume), `vw`
(volume-weighted average price), `n` (number of trades) and `t`, the start of the bar in Unix milliseconds; `limit`
accepts up to 50,000 bars per request, with 5,000 by default[^3].

An illustrative sample with that structure (the numbers are from NOVA, the course's fictitious stock):

```python
import json

texto = """{
  "ticker": "NOVA", "adjusted": false, "queryCount": 2,
  "resultsCount": 2, "status": "OK", "request_id": "ej-0001",
  "results": [
    {"t": 1748836800000, "o": 53.35, "h": 53.94, "l": 52.55,
     "c": 53.12, "v": 8888714, "vw": 53.21, "n": 50946},
    {"t": 1748923200000, "o": 53.05, "h": 53.4, "l": 52.6,
     "c": 52.92, "v": 6151230, "vw": 52.98, "n": 41210}
  ],
  "next_url": "https://api.massive.com/v2/aggs/...?cursor=ej"
}"""
respuesta = json.loads(texto)
barras = pd.DataFrame(respuesta["results"])
barras["t"] = pd.to_datetime(barras["t"], unit="ms", utc=True)
print(respuesta["status"], respuesta["resultsCount"],
      "next_url" in respuesta)
barras.set_index("t")
```
```output
OK 2 True
                               o      h      l      c        v     vw      n
t
2025-06-02 04:00:00+00:00  53.35  53.94  52.55  53.12  8888714  53.21  50946
2025-06-03 04:00:00+00:00  53.05  53.40  52.60  52.92  6151230  52.98  41210
```

Two things worth noting. The timestamp of a daily bar is midnight in **New York** expressed in UTC: 04:00 in summer
and 05:00 in winter, as in the documentation's example. In a daily bar, the UTC date matches the New York date; in
intraday bars it does not: a 21:00 New York bar is already the next day in UTC. That is why you convert to the
market's time zone (`.tz_convert("America/New_York")`) before keeping the date. And `adjusted` says whether the
prices are adjusted for splits; lesson 1.3.8 explains why you always need to know it.

## A client that paginates and respects the limit

A REST client for these APIs has to do three things well: authenticate without leaving the key in the code, follow
the pagination to the end and react to limit errors (HTTP 429) by waiting as long as the server says.

```mermaid
flowchart TB
  U[Initial URL] --> G[GET with key]
  G --> C{Status code?}
  C -- 429 --> W[Retry-After]
  W --> G
  C -- 200 --> R[Store results]
  R --> N{next_url?}
  N -- yes --> G
  N -- no --> F[End]
```

To run it for real without a network or a key, the next cell starts a **local server** in a thread that mimics the
API: it serves NOVA's 502 sessions of 2024 and 2025 in the format of the previous section, in pages of `limit` bars
with a cursor in `next_url`, requires the key in `apiKey` and responds 429 with `Retry-After: 1` if it receives more
than 5 requests in one second.

```python
import threading
import time
from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse

nova = pd.read_csv("nova_diario_sin_ajustar.csv",
                   parse_dates=["fecha"])
nova = nova[nova["fecha"] >= "2024-01-01"]
abre_ny = (nova["fecha"].dt.tz_localize("America/New_York")
           .dt.tz_convert("UTC"))
BARRAS = [
    {"t": int(ts.timestamp() * 1000), "o": f.apertura,
     "h": f.maximo, "l": f.minimo, "c": f.cierre, "v": f.volumen}
    for ts, f in zip(abre_ny, nova.itertuples())
]

class ApiFalsa(BaseHTTPRequestHandler):
    recientes = deque()          # times of the accepted requests
    contador = {"200": 0, "401": 0, "429": 0}

    def responder(self, codigo, cuerpo, cabeceras=()):
        self.contador[str(codigo)] += 1
        self.send_response(codigo)
        self.send_header("Content-Type", "application/json")
        for k, v in cabeceras:
            self.send_header(k, v)
        self.end_headers()
        self.wfile.write(json.dumps(cuerpo).encode())

    def do_GET(self):
        q = parse_qs(urlparse(self.path).query)
        if q.get("apiKey") != ["test-key"]:
            return self.responder(401, {"status": "ERROR"})
        ahora = time.monotonic()
        while self.recientes and ahora - self.recientes[0] > 1:
            self.recientes.popleft()
        if len(self.recientes) >= 5:
            return self.responder(429, {"status": "ERROR"},
                                  [("Retry-After", "1")])
        self.recientes.append(ahora)
        inicio = int(q.get("cursor", ["0"])[0])
        limite = int(q.get("limit", ["50"])[0])
        pagina = BARRAS[inicio:inicio + limite]
        cuerpo = {"status": "OK", "resultsCount": len(pagina),
                  "results": pagina}
        if inicio + limite < len(BARRAS):
            base = f"http://127.0.0.1:{self.server.server_port}"
            cuerpo["next_url"] = (f"{base}/v2/aggs/ticker/NOVA?"
                                  f"cursor={inicio + limite}"
                                  f"&limit={limite}")
        self.responder(200, cuerpo)

    def log_message(self, *args):    # no logging in the output
        pass

servidor = ThreadingHTTPServer(("127.0.0.1", 0), ApiFalsa)
threading.Thread(target=servidor.serve_forever, daemon=True).start()
URL = f"http://127.0.0.1:{servidor.server_port}/v2/aggs/ticker/NOVA"
print(len(BARRAS), "bars served")
```
```output
502 bars served
```

Port `0` asks the system for a free one, so the cell does not clash with anything that is already listening. Now the
client, with the standard library (`urllib`): without the key, the server responds 401 and `urlopen` raises an
exception.

```python raises
from urllib.error import HTTPError
from urllib.request import urlopen

urlopen(f"{URL}?limit=50", timeout=5)
```
```output
urllib.error.HTTPError: HTTP Error 401: Unauthorized
```

The key is read from an environment variable. In the cell it is set by hand for the local server; in a real program
it would be in the environment or in a `.env` file outside the repository (lesson 0.2.4):

```python
import os
from urllib.parse import urlencode

os.environ.setdefault("API_KEY", "test-key")  # only here

def get_json(url, intentos=5):
    sep = "&" if "?" in url else "?"
    clave = urlencode({"apiKey": os.environ["API_KEY"]})
    firmada = f"{url}{sep}{clave}"
    for _ in range(intentos):
        try:
            with urlopen(firmada, timeout=5) as r:
                return json.load(r)
        except HTTPError as e:
            if e.code != 429:
                raise
            time.sleep(float(e.headers.get("Retry-After", "1")))
    raise RuntimeError(f"still 429 after {intentos} attempts")

def descargar_todo(url):
    filas, paginas = [], 0
    while url:
        datos = get_json(url)
        filas += datos["results"]
        paginas += 1
        url = datos.get("next_url")
    return filas, paginas

filas, paginas = descargar_todo(f"{URL}?limit=50")
print(len(filas), "bars in", paginas, "pages;",
      ApiFalsa.contador["429"], "responses with code 429")
```
```output
502 bars in 11 pages; 2 responses with code 429
```

502 bars in 11 pages of 50 (the last one, of 2). The server only accepts 5 requests per second, so the client
receives two 429s along the way: after the fifth page and after the tenth. On each one it waits the second that
`Retry-After` asks for and repeats **the same** request; it does not skip a page or start from scratch. If the server
were still returning 429 after several attempts, the client gives up with a clear error instead of staying in a
loop. Lesson 1.4.1 adds what is missing for production: exponential backoff with jitter, network errors and 5xx, and
websockets.

```python
df = pd.DataFrame(filas)
df["t"] = pd.to_datetime(df["t"], unit="ms", utc=True)
print(df["t"].is_unique, df["t"].is_monotonic_increasing)
print(df["t"].iloc[0], "->", df["t"].iloc[-1])
servidor.shutdown()
```
```output
True True
2024-01-02 05:00:00+00:00 -> 2025-12-31 05:00:00+00:00
```

The final check is the usual one: unique and increasing timestamps, and the first and the last where they were
expected. Badly done pagination usually leaves duplicates at the page boundaries or loses the last page.

## The official client

With the official client, all of the above stays inside the library: the pagination is walked with an iterator and
the key goes in the constructor. The example in the Massive documentation has this shape (it needs a network and a
key):

```python skip title="massive_barras.py"
import os

from massive import RESTClient       # uv add massive

client = RESTClient(os.environ["MASSIVE_API_KEY"])
barras = list(client.list_aggs(
    "AAPL", 1, "day", "2025-01-01", "2025-12-31",
    adjusted="true", sort="asc", limit=50000,
))
print(len(barras), barras[0])
```

Official clients save work, but it is worth having written one by hand once: when a download fails at three in the
morning, you need to know what a 429 is, what a cursor is and where the last page got lost.

## How much an intraday download costs

With the format and the limits you can already budget a large download. One-minute bars of the US regular session
(390 a day, 252 sessions a year), 500 stocks and 5 years, with a maximum of 50,000 bars per request:

```python
barras_ano = 390 * 252
acciones, anos, max_barras = 500, 5, 50_000
por_accion = math.ceil(barras_ano * anos / max_barras)
total = acciones * por_accion
print(f"{barras_ano * anos * acciones:,} bars;"
      f" {por_accion} requests per stock; {total:,} in total")
for plan, por_min in [("5 per minute", 5), ("200 per minute", 200)]:
    print(f"{plan}: {total / por_min / 60:.1f} hours")
```
```output
245,700,000 bars; 10 requests per stock; 5,000 in total
5 per minute: 16.7 hours
200 per minute: 0.4 hours
```

Almost 246 million bars fit in 5,000 requests, because each one carries 50,000 bars. At 5 requests per minute it
would be almost 17 hours; at 200, 25 minutes. But the limit is only one constraint: the Massive Basic plan does not
have minute bars from 5 years ago (its history is 2), so the real calculation starts by checking which plan has that
data. And then there is storage: 246 million rows in CSV are tens of gigabytes; in Parquet, much less (lesson 1.4.7).

## Exercises

1. Repeat the estimate of days for the daily history of 500 stocks, but assuming you can only use 50% of the daily
   limit (your production system needs the rest). Which plans still fit in one day?

**Solution.**

```python
limites["dias_50"] = (peticiones / (0.5 * limites["al_dia"])
                      ).apply(math.ceil)
limites.loc[limites["dias_50"] == 1].index.tolist()
```
```output
['Massive Basic', 'Alpaca Basic', 'Tiingo Starter']
```

With half the limit, Massive Basic (3,600 a day), Alpaca Basic and, just barely, Tiingo (500 of 500) still fit in one
day; the others do not. A production system consumes quota (quotes, checks), and a historical download that eats it
all leaves the system without data. It is best to keep separate keys or plans for the two things.

2. Modify the local server so that it accepts only 3 requests per second and download again with `limit=50`. How many
   429s does the client receive? And with `limit=200`?

**Solution.**

```python
def probar(maximo, limit):
    ApiFalsa.contador["429"] = 0
    ApiFalsa.recientes.clear()
    original = ApiFalsa.do_GET

    def do_get_estricto(self):
        while len(self.recientes) > maximo - 1:
            if time.monotonic() - self.recientes[0] > 1:
                self.recientes.popleft()
            else:
                return self.responder(429, {}, [("Retry-After", "1")])
        original(self)

    ApiFalsa.do_GET = do_get_estricto
    srv = ThreadingHTTPServer(("127.0.0.1", 0), ApiFalsa)
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    url = f"http://127.0.0.1:{srv.server_port}/v2/aggs/ticker/NOVA"
    filas, paginas = descargar_todo(f"{url}?limit={limit}")
    srv.shutdown()
    ApiFalsa.do_GET = original
    return len(filas), paginas, ApiFalsa.contador["429"]

print(probar(3, 50), probar(3, 200))
```
```output
(502, 11, 3) (502, 3, 0)
```

With 3 requests per second and pages of 50, the client receives a 429 every three pages; with pages of 200, 3
requests are enough and it receives none. Requesting large pages (up to the maximum the API accepts) is the simplest
way to spend less quota and to wait less.

3. Your system trades 50 US stocks daily with closing data and needs 10 years of adjusted history. Choose among the
   plans in the lesson and justify what you would check before paying.

**Solution.**

End of day is enough, so neither real time nor intraday is needed. Ten years are needed: that rules out the Massive
Basic plan (2 years), Starter (5 years), EODHD's free plan (1 year) and FMP's Starter (up to 5). What remains, among
others, are the end-of-day plans with long history (Tiingo, more than 30 years; EODHD All-World, more than 30) or
Massive's Developer (10 years, at a higher price). Before paying, with the free plan: whether it provides both raw
and adjusted prices, and the dividends and splits (lesson 1.3.8); whether it has delisted stocks; the real limits
against 50 stocks; and the license, if the system is going to publish anything or be used with third-party money
(lesson 1.3.7).

[^1]: Massive, [“Polygon.io is Now Massive”](https://massive.com/blog/polygon-is-now-massive), October 30, 2025, and [pricing page](https://massive.com/pricing), accessed on October 21, 2026.
[^2]: Pricing pages accessed on October 21, 2026: [Alpaca](https://docs.alpaca.markets/us/docs/about-market-data-api), [Tiingo](https://www.tiingo.com/about/pricing), [EODHD](https://eodhd.com/pricing), [Alpha Vantage](https://www.alphavantage.co/premium/), [Twelve Data](https://twelvedata.com/pricing), [Financial Modeling Prep](https://site.financialmodelingprep.com/developer/docs/pricing).
[^3]: Massive, [Custom Bars (OHLC)](https://massive.com/docs/rest/stocks/aggregates/custom-bars) and [REST API Quickstart](https://massive.com/docs/rest/quickstart), accessed on October 21, 2026.

## Self-assessment

1. Which Massive stocks plan provides real-time data, according to its pricing page as of October 21, 2026?

   - A. Basic (free)
   - B. Advanced (199 USD a month)
   - C. Starter (29 USD a month)
   - D. Developer (79 USD a month)

   *Answer B: Advanced (199 USD a month).* Basic provides end-of-day data; Starter and Developer, with a 15-minute delay; only Advanced provides real time (and, according to the page, only for non-professional users).

2. An API responds with `next_url` in the JSON. What should the client do?

   - A. Request that URL (adding the key) until the response no longer carries `next_url`
   - B. Ignore it if it already has data
   - C. Increase `limit` until it disappears
   - D. Request the same URL again

   *Answer A: Request that URL (adding the key) until the response no longer carries `next_url`.* `next_url` carries a cursor with the point where the query continues. If it is not followed, the download stays on the first page without any error.

3. The server responds 429 with the header `Retry-After: 1`. What is the right thing to do?

   - A. Retry immediately in a loop
   - B. Move on to the next page
   - C. Abort the download
   - D. Wait at least one second and repeat the same request

   *Answer D: Wait at least one second and repeat the same request.* 429 means too many requests. `Retry-After` says how long to wait. Retrying in a loop only prolongs the block; skipping a page loses data.

4. With the Alpha Vantage free plan (25 requests a day), how many days does it take to download the daily history of 500 stocks if each one fits in one request?

   - A. One
   - B. Five
   - C. Twenty
   - D. One hundred

   *Answer C: Twenty.* 500 requests divided by 25 a day is 20 days. A daily limit changes the architecture: you need to store what has been downloaded and continue the next day (lesson 1.4.8).

5. Why should the API key not be written in the code?

   - A. Because it takes up a lot of space
   - B. Because the code ends up in a repository, a shared notebook or a traceback, and the key gives access to your account and your quota
   - C. Because APIs do not accept keys in the code
   - D. Because it changes every day

   *Answer B: Because the code ends up in a repository, a shared notebook or a traceback, and the key gives access to your account and your quota.* It is read from an environment variable or from a file outside version control (lesson 0.2.4). A key that has been published must be revoked.

## Key points

- The retail and prosumer tier costs from zero to a few tens of dollars a month; what changes from one plan to another is the history, the delay (end of day, 15 minutes, real time) and the request limit.
- Polygon.io has been called Massive since October 30, 2025; the old domain redirects and the official Python client is now `massive`.
- Limits come in different units (per minute, per hour, per day): you have to convert them to the same one before comparing, and use them to estimate how long the download takes.
- A serious client follows the pagination (`next_url`, `next_page_token`), respects 429s and `Retry-After`, and reads the key from an environment variable.
- Prices and plans change: cite them with the date on which you read them and read them again before paying.

## Resources

### Research

- [Python for Trading Course - Part 1: Market Data with OpenBB](https://pythonparatrading.com/en/openbb-free-market-data/) · Research-0014 · 10 min

### Documentation

- [Massive: pricing (stocks plans)](https://massive.com/pricing) · massive.com
- [Massive: “Polygon.io is Now Massive” (October 30, 2025)](https://massive.com/blog/polygon-is-now-massive) · massive.com
- [Massive: Custom Bars (OHLC), aggregates endpoint](https://massive.com/docs/rest/stocks/aggregates/custom-bars) · massive.com
- [Alpaca: About Market Data API (Basic and Algo Trader Plus plans)](https://docs.alpaca.markets/us/docs/about-market-data-api) · docs.alpaca.markets

### Data

- [Level 1 data: NOVA daily unadjusted (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/nova_diario_sin_ajustar.csv) · Download · nova_diario_sin_ajustar.csv

Previous lesson: [1.3.2 · Free market data sources](https://pythonparatrading.com/en/python-trading-course/level-1/free-market-data-sources/)

Next lesson: [1.3.4 · Professional market data providers](https://pythonparatrading.com/en/python-trading-course/level-1/professional-market-data-providers/)
