Next free-source hunt
We check for new free financial data every Wednesday.
Datasets / Macro
bls.gov/

U.S. Bureau of Labor Statistics (BLS) Public Data API

Freeauth: none (v1); free registration key for v2Custom#macro#inflation#employment#economic-releases✓ verified Oct 21, 2026

U.S. Bureau of Labor Statistics (BLS) Public Data API

Official REST API from the U.S. Bureau of Labor Statistics for inflation, employment, and wage data. Public domain.

The U.S. Bureau of Labor Statistics publishes official economic data through free REST APIs at api.bls.gov. No registration required for v1.0 (25 queries/day); free registration for v2.0 (email and organization name, renewed yearly) unlocks 500 queries/day, 50 series and 20 years of data per query. BLS material is public domain, except previously copyrighted photographs and illustrations; BLS asks that you cite it as the source.

Required Attribution:

Cite BLS (as stated in this row's license note)

Coverage

CPI, PPI, unemployment, payrolls, productivity, wages; US national and regional data; historical coverage varies by series (CPI from 1913, employment from 1939)

Good for

  • Tracking inflation and employment data for macro trading signals
  • Building economic calendars and release monitors
  • Training macro forecasting models on official government statistics

Verdict

Tracking official economic releases for macro trading signals. CPI-U (CUUR0000SA0), unemployment rate (LNS14000000), nonfarm payrolls (CES0000000001), and PPI (WPUFD4) updated monthly. V2.0 free registration unlocks higher limits and calculated changes.

Endpoints

EndpointReturns
POST /publicAPI/v2/timeseries/data/Retrieve time series data for one or more series IDs

Get the free weekly email

Free weekly email of newly verified free data. Unsubscribe by emailing us.

Use in your agent

Load BLS time series into pandas and Parquet. Checked against the provider's documentation on Oct 29, 2026.

Raw skill file (/skills/bls-public-data.md)Also in our MCP server: call get_ingestion_skill with {"slug": "bls-public-data"}.

Access

  • Version 1.0: no registration or key.
  • Version 2.0: register at https://data.bls.gov/registrationEngine/ with your email and organization name; the key arrives by email and must be renewed at least once a year.

Request

bash
curl -X POST -H 'Content-Type: application/json' -d '{"seriesid":["CUUR0000SA0"],"startyear":"2017","endyear":"2026"}' https://api.bls.gov/publicAPI/v1/timeseries/data/

Format

JSON: status, message and Results.series, each with seriesID and data points {year, period (M01 to M12), periodName, value, footnotes}. Values arrive as strings. You need to know the series IDs; BLS has no catalogue of them.

Load into pandas and Parquet

Needs Python 3 with requests, pandas and pyarrow.

python
"""BLS Public Data API v1 (CPI-U and unemployment rate) -> pandas -> Parquet. No key for v1."""
import requests
import pandas as pd

payload = {"seriesid": ["CUUR0000SA0", "LNS14000000"],   # CPI-U all items (NSA); unemployment rate (SA)
           "startyear": "2017", "endyear": "2026"}          # v1 allows up to 10 years per request
r = requests.post("https://api.bls.gov/publicAPI/v1/timeseries/data/", json=payload, timeout=60)
r.raise_for_status()
body = r.json()
if body["status"] != "REQUEST_SUCCEEDED":
    raise RuntimeError(body["message"])
for m in body["message"]:                    # some problems arrive as messages on a "successful" reply
    print("BLS message:", m)
empty = [s["seriesID"] for s in body["Results"]["series"] if not s["data"]]
if empty:
    raise RuntimeError(f"no data for {empty}: {body['message']}")

rows = []
for s in body["Results"]["series"]:
    for p in s["data"]:
        rows.append({"series_id": s["seriesID"], "year": int(p["year"]), "period": p["period"],
                     "value": p["value"],
                     "footnotes": "; ".join(f.get("text", "") for f in p["footnotes"] if f)})
df = pd.DataFrame(rows)
df = df[df["period"].str.match(r"M(0[1-9]|1[0-2])$")]   # drop M13 (annual average) if present
df["date"] = pd.to_datetime(df["year"].astype(str) + "-" + df["period"].str[1:] + "-01")
df["value"] = pd.to_numeric(df["value"], errors="coerce")  # strings; "-" marks a missing value
df = df.sort_values(["series_id", "date"])
df.to_parquet("bls_cpi_unemployment.parquet", index=False)
print(len(df), "rows;", df.groupby("series_id")["date"].agg(["min", "max"]).astype(str).to_dict("index"))

Run by us on 2026-10-29 (python 3.13.5 | pandas 3.1.0 | pyarrow 26.0.0 | requests 2.34.2). Result: 234 monthly rows (CPI-U and the unemployment rate), 2017-01 to 2026-09.

Limits

  • Version 1.0: 25 queries a day, 25 series and 10 years per query.
  • Version 2.0 (free registration): 500 queries a day, 50 series and 20 years per query.
  • Both: 50 requests per 10 seconds; going over returns HTTP 429.

Quirks

  • Some problems come back as REQUEST_SUCCEEDED with a message and an empty data list (BLS's own example is an invalid series ID). The snippet prints messages and stops on empty series.
  • Observed 2026-10-29: a version 1.0 request for 2010 to 2026 returned REQUEST_SUCCEEDED with the message "Year range has been reduced to the system-allowed limit of 10 years." and only 2010 to 2019. Split long ranges.
  • Missing values are "-" with a footnote; October 2025 CPI and unemployment read "Data unavailable due to the 2025 lapse in appropriations". The snippet turns them into NaN.
  • Series IDs must be upper case.

Terms and attribution

  • BLS material is in the public domain; BLS asks you to cite it as the source.

Sources

Information and education only. Not financial, investment, legal or tax advice. Listings can be inaccurate, incomplete or out of date and are provided "as is". Free tiers, limits and licenses belong to each provider and can change, so check the provider's own terms before you rely on them. Terms & Disclaimer