---
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."

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.
