---
name: bls-public-data-ingestion
description: "How to get U.S. Bureau of Labor Statistics (BLS) Public Data API data into a pandas DataFrame and Parquet: access, the request, the format, limits, quirks and terms."
---

# Load BLS time series into pandas and Parquet

Source entry: https://signal-lab-jet.vercel.app/datasets/bls-public-data
Provider: https://www.bls.gov/developers/
Checked against the provider's documentation: 2026-10-29

## 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.
- Terms page: https://www.bls.gov/opub/copyright-information.htm

## Sources
- https://www.bls.gov/developers/api_faqs.htm: "Unregistered users may request up to 25 queries daily."
- https://www.bls.gov/developers/api_faqs.htm: "Registered users may request up to 20 years per query."
- https://www.bls.gov/developers/api_faqs.htm: "Users must renew registration with the BLS Public Data API at least once a year."
- https://www.bls.gov/developers/api_faqs.htm: "We do not currently have a catalogue of series IDs"
- https://www.bls.gov/developers/api_signature.htm: "You can include up to 25 series IDs, each separated with a comma, in the body of your request."
- https://www.bls.gov/opub/copyright-information.htm: "You are free to use our public domain material without specific permission, although we do ask that you cite the Bureau of Labor Statistics as the source."

## Help improve this skill
Missing a source, or see something to improve? Call the MCP tool suggest_source or feedback (or POST /api/suggest or /api/feedback). Reviewed ideas: https://signal-lab-jet.vercel.app/agent-suggestions

Information for educational purposes only. Free tiers, limits and licenses belong to the providers and may change; check the provider's own terms before using the data.
