Source on EconIndx: Bureau of Labor Statistics β free, register for v2 key (500 series/day), covers CPI, payrolls, unemployment, wages, and more.
Access & Pricing
Free. Register for a v2 API key at data.bls.gov/registrationEngine β email + organization name, instant activation. No cost, no contracts, no enterprise tier. The key unlocks 500 series/day and 50 series/request (vs 25/day and 10 series unregistered).
Your First Data Pull
BLS uses a POST-based API with JSON bodies. Series IDs are structured strings that encode the survey, geography, and metric:
import requests
import json
import pandas as pd
BLS_KEY = "your_key_here"
BLS_URL = "https://api.bls.gov/publicAPI/v2/timeseries/data/"
def fetch_bls(series_ids: list[str], start_year: str, end_year: str) -> pd.DataFrame:
payload = {
"seriesid": series_ids,
"startyear": start_year,
"endyear": end_year,
"registrationkey": BLS_KEY,
}
r = requests.post(BLS_URL, data=json.dumps(payload),
headers={"Content-type": "application/json"})
results = r.json()["Results"]["series"]
rows = []
for series in results:
sid = series["seriesID"]
for obs in series["data"]:
rows.append({
"series_id": sid,
"year": int(obs["year"]),
"period": obs["period"], # M01-M12, Q01-Q04, or A01
"value": float(obs["value"]),
"footnotes": obs.get("footnotes", []),
})
return pd.DataFrame(rows)
# Core US labor market series
df = fetch_bls(
series_ids=[
"LNS14000000", # Unemployment rate, SA
"CES0000000001", # Total nonfarm payrolls (thousands)
"CUUR0000SA0", # CPI-U all items, not seasonally adjusted
],
start_year="2015",
end_year="2026"
)
print(df.groupby("series_id")[["value"]].agg(["count", "min", "max"]))
First Pull: What to Expect
| Series ID | Description | Frequency | Rows (2015β2026) | History |
|---|---|---|---|---|
LNS14000000 | Unemployment rate, SA | Monthly | ~132 | Back to 1948 |
CES0000000001 | Total nonfarm payrolls | Monthly | ~132 | Back to 1939 |
CUUR0000SA0 | CPI-U, all items, NSA | Monthly | ~132 | Back to 1913 |
PRS85006032 | Nonfarm business productivity | Quarterly | ~44 | Back to 1947 |
LES1252881600Q | Median usual weekly earnings | Quarterly | ~44 | Back to 1979 |
π‘ Tip: BLS series IDs encode the survey, seasonal adjustment, area, and item. For example,
CUSR0000SA0breaks down as:CU= CPI-U,S= seasonally adjusted,R= US city average,0000= all items,SA0= all items index. Use the BLS Series ID formats reference to decode any ID.
Batch of 3 series (10 years): returns in under 2 seconds. BLS API v2 maxes at 50 series per request and 20 years per request β for longer history, make multiple calls with different year ranges.
Footnote codes to track: BLS embeds revision flags in the footnotes array. Key codes: P = preliminary (will be revised), R = revised, C = corrected. Always store footnotes β payroll numbers carry P for 2 months after release.
Key Series IDs to Know
BLS series IDs encode metadata in their structure. The pattern for most surveys:
[Survey prefix][Seasonal][Area code][Measure][...]
CPS (unemployment):
LNS14000000β unemployment rate, national, SALNS11000000β labor force participation rate, SALNU04000000β unemployment level (thousands), NSA
CES (payrolls):
CES0000000001β total nonfarm, SACES3000000001β manufacturing, SACES9000000001β government, SACEU0000000008β average hourly earnings, all private, SA
CPI:
CUUR0000SA0β CPI-U all items, NSA (use this for official CPI)CUSR0000SA0β CPI-U all items, SACUUR0000SA0L1Eβ Core CPI (less food & energy), NSA
JOLTS (job openings):
JTS000000000000000JOLβ job openings levelJTS000000000000000HILβ hires levelJTS000000000000000QULβ quits level
Data Tolerance & Validation
Whatβs normal:
- Payroll data (
CES) is revised in the two months following release. Build your pipeline to re-pull the last 3 months on every run. - CPI is rarely revised after initial release β treat as final.
- Footnote
P(preliminary) on payrolls is expected for the most recent 1β2 months. - Period codes:
M01βM12= months JanβDec,Q01βQ04= quarters,A01= annual average.
β οΈ Revision note: BLS employment data (
PAYEMSequivalent:CES0000000001) is revised for two months after initial release. The annual benchmark revision, published each February, can revise multiple years of data. Re-pull the full series after each benchmark release.
Validation checks:
def validate_bls_pull(df: pd.DataFrame) -> dict:
by_series = df.groupby("series_id").agg(
row_count=("value", "count"),
latest_year=("year", "max"),
has_preliminary=("footnotes", lambda x: any(
any(f.get("code") == "P" for f in row) for row in x if row
))
).reset_index()
for _, row in by_series.iterrows():
stale = (2026 - row["latest_year"]) > 1
print(f"{row['series_id']}: {row['row_count']} rows, "
f"latest={row['latest_year']}, stale={stale}, "
f"has_prelim={row['has_preliminary']}")
validate_bls_pull(df)
Alert thresholds:
- Monthly series: alert if latest period is more than 45 days old
- Quarterly series: alert if latest period is more than 120 days old
- Any series where
has_preliminary=Truefor a period older than 3 months: re-pull
Getting Flat Files (Bulk Alternative)
For initial full loads, BLS bulk flat files at download.bls.gov are faster than the API:
# All CPS data β tab-delimited flat file
url = "https://download.bls.gov/pub/time.series/ln/ln.data.1.AllData"
df_bulk = pd.read_csv(url, sep="\t", dtype=str)
df_bulk.columns = df_bulk.columns.str.strip()
print(f"Full CPS dataset: {len(df_bulk):,} rows")
# Expected: ~500,000β800,000 rows for the full CPS series
Flat files are updated after each release. Use them for initial backfills, then switch to the API for incremental updates.
Schema Stability
BLS series IDs are highly stable β they rarely retire a series. The API response envelope has been consistent for years. The only change pattern to watch: BLS occasionally reclassifies industries in CES (payrolls), which can shift historical values for industry breakdowns. Monitor the footnotes field for revision notices on any series youβre tracking closely.