# FRED — Federal Reserve Economic Data `https://fred.stlouisfed.org` / `https://api.stlouisfed.org` — the canonical source for US macroeconomic time series (800,000+ series). The REST API at `api.stlouisfed.org` requires a free registered key. The web endpoints at `fred.stlouisfed.org` (CSV, JSON, HTML) are all blocked to headless HTTP — they consistently timeout with no response. For zero-key access use the BLS API (unemployment, CPI, payrolls) or World Bank API (GDP, growth rates, annual data). ## Do this first **Decision tree: pick one approach.** ``` Need GDP, CPI, UNRATE, payrolls only? → use BLS + World Bank (no key, free forever) Need FEDFUNDS, DGS10, SP500, any FRED series? → get a free FRED API key (5 min) Need browser-visible chart data? → use CDP to intercept network requests ``` **The web CSV/JSON/TXT URLs all timeout — do NOT attempt them:** ```python # ALL OF THESE TIMEOUT — confirmed dead from headless HTTP: # https://fred.stlouisfed.org/graph/fredgraph.csv?id=GDP ← timeout # https://fred.stlouisfed.org/graph/fredgraph.json?id=GDP ← timeout # https://fred.stlouisfed.org/data/GDP.txt ← timeout # https://fred.stlouisfed.org/series/GDP ← timeout ``` ## Getting a free FRED API key 1. Go to `https://fred.stlouisfed.org/docs/api/api_key.html` 2. Click "Request or view your API Keys" 3. Sign in / register (free St. Louis Fed account) 4. Key appears immediately — it's a 32-character lowercase alphanumeric string The key is free, instant, and unlimited for reasonable use (120 req/min cap). --- ## Option A: FRED REST API (requires free key, 800K+ series) The only way to get FRED data programmatically. Set `FRED_KEY` in your `.env` file. ```python import json, os FRED_KEY = os.environ["FRED_KEY"] # 32-char lowercase alphanumeric BASE = "https://api.stlouisfed.org/fred" ``` ### Series metadata ```python import json, os FRED_KEY = os.environ["FRED_KEY"] BASE = "https://api.stlouisfed.org/fred" meta = json.loads(http_get(f"{BASE}/series?series_id=GDP&api_key={FRED_KEY}&file_type=json")) s = meta['seriess'][0] print(s['title']) # "Gross Domestic Product" print(s['observation_start']) # "1947-01-01" print(s['observation_end']) # "2025-10-01" print(s['frequency']) # "Quarterly" print(s['frequency_short']) # "Q" print(s['units']) # "Billions of Dollars" print(s['units_short']) # "Bil. of $" print(s['seasonal_adjustment']) # "Seasonally Adjusted Annual Rate" print(s['popularity']) # 81 (0-100) print(s['last_updated']) # "2025-12-19 08:00:06-06" ``` ### Observations (the actual data) ```python import json, os FRED_KEY = os.environ["FRED_KEY"] BASE = "https://api.stlouisfed.org/fred" # Latest 10 values, most recent first obs = json.loads(http_get( f"{BASE}/series/observations" f"?series_id=GDP" f"&api_key={FRED_KEY}" f"&file_type=json" f"&limit=10" f"&sort_order=desc" # "desc" = newest first, "asc" = oldest first (default) )) print(obs['count']) # 314 (total observations) print(obs['observation_start']) # "1947-01-01" (what's in the full series) for o in obs['observations']: date = o['date'] # "2025-10-01" value = o['value'] # "29726.4" — always a STRING, may be "." for missing if value != '.': print(f"{date}: ${float(value):,.1f}B") # 2025-10-01: $29,726.4B # 2025-07-01: $29,339.1B # 2025-04-01: $29,119.3B ``` ### Date-range filtering ```python import json, os FRED_KEY = os.environ["FRED_KEY"] BASE = "https://api.stlouisfed.org/fred" obs = json.loads(http_get( f"{BASE}/series/observations" f"?series_id=UNRATE" f"&api_key={FRED_KEY}" f"&file_type=json" f"&observation_start=2020-01-01" f"&observation_end=2024-12-31" f"&sort_order=desc" )) for o in obs['observations'][:5]: print(f"{o['date']}: {o['value']}%") # 2024-12-01: 4.1% # 2024-11-01: 4.2% # 2024-10-01: 4.1% ``` ### Key series IDs | FRED ID | Description | Frequency | Unit | |---------|-------------|-----------|------| | `GDP` | Gross Domestic Product | Quarterly | Billions of $, SAAR | | `GDPC1` | Real GDP (chained 2017 $) | Quarterly | Billions of chained 2017 $ | | `UNRATE` | Unemployment Rate | Monthly | Percent, SA | | `CPIAUCSL` | CPI: All Urban Consumers, SA | Monthly | Index 1982-84=100 | | `CPIAUCNS` | CPI: All Urban Consumers, not SA | Monthly | Index 1982-84=100 | | `FEDFUNDS` | Federal Funds Effective Rate | Monthly | Percent | | `DFF` | Federal Funds Rate (daily) | Daily | Percent | | `DGS10` | 10-Year Treasury Constant Maturity | Daily | Percent | | `DGS2` | 2-Year Treasury | Daily | Percent | | `SP500` | S&P 500 | Daily | Index | | `NASDAQCOM` | NASDAQ Composite | Daily | Index | | `PAYEMS` | Total Nonfarm Payrolls | Monthly | Thousands of persons, SA | | `PCEPI` | PCE Price Index | Monthly | Index 2017=100, SA | | `PCEPILFE` | Core PCE Price Index | Monthly | Index 2017=100, SA | | `DCOILBRENTEU` | Brent Crude Oil | Daily | $ per Barrel | | `DEXUSEU` | USD/EUR Exchange Rate | Daily | USD per EUR | | `M2SL` | M2 Money Stock | Monthly | Billions of $, SA | | `MORTGAGE30US` | 30-Year Fixed Mortgage Rate | Weekly | Percent | ### Series search ```python import json, os FRED_KEY = os.environ["FRED_KEY"] BASE = "https://api.stlouisfed.org/fred" results = json.loads(http_get( f"{BASE}/series/search" f"?search_text=unemployment+rate" f"&api_key={FRED_KEY}" f"&file_type=json" f"&limit=5" f"&order_by=popularity" # "popularity" | "search_rank" | "series_id" | "title" | "units" | "frequency" | "seasonal_adjustment" | "realtime_start" | "realtime_end" | "last_updated" | "observation_start" | "observation_end" f"&sort_order=desc" # most popular first )) for s in results['seriess']: print(f"{s['id']}: {s['title']} ({s['frequency_short']}, {s['units_short']})") # UNRATE: Unemployment Rate (M, %) # UNEMPLOY: Unemployment Level (M, Thous. of Persons) ``` ### Multiple series — parallel fetch ```python import json, os from concurrent.futures import ThreadPoolExecutor FRED_KEY = os.environ["FRED_KEY"] BASE = "https://api.stlouisfed.org/fred" def fetch_latest(series_id): obs = json.loads(http_get( f"{BASE}/series/observations?series_id={series_id}" f"&api_key={FRED_KEY}&file_type=json&limit=1&sort_order=desc" )) o = obs['observations'][0] return series_id, o['date'], o['value'] series_ids = ["GDP", "UNRATE", "CPIAUCSL", "FEDFUNDS", "DGS10", "SP500"] with ThreadPoolExecutor(max_workers=6) as ex: results = list(ex.map(fetch_latest, series_ids)) for sid, date, val in results: print(f"{sid:15} {date}: {val}") # GDP 2025-10-01: 29726.4 # UNRATE 2026-03-01: 4.3 # CPIAUCSL 2026-02-01: 321.457 # FEDFUNDS 2026-03-01: 4.33 # DGS10 2026-04-17: 4.34 # SP500 2026-04-17: 5282.70 # Confirmed: 6 parallel requests complete in ~0.4s ``` ### Parse observations into a list of (date, float) tuples ```python import json, os FRED_KEY = os.environ["FRED_KEY"] BASE = "https://api.stlouisfed.org/fred" obs = json.loads(http_get( f"{BASE}/series/observations?series_id=DGS10&api_key={FRED_KEY}&file_type=json" f"&observation_start=2024-01-01&sort_order=asc" )) data = [ (o['date'], float(o['value'])) for o in obs['observations'] if o['value'] != '.' # '.' = missing value, skip it ] print(f"{len(data)} observations") print(f"First: {data[0]}") # ('2024-01-02', 3.91) print(f"Last: {data[-1]}") # ('2026-04-17', 4.34) ``` ### Handle errors ```python import urllib.error, json try: r = http_get(f"https://api.stlouisfed.org/fred/series?series_id=BADID&api_key={FRED_KEY}&file_type=json") print(json.loads(r)) except urllib.error.HTTPError as e: err = json.loads(e.read().decode()) # err['error_code'] → 400 # err['error_message'] → "Bad Request. The series does not exist." print(f"FRED error {err['error_code']}: {err['error_message']}") ``` --- ## Option B: BLS API (no key required, confirmed live) Bureau of Labor Statistics. Covers unemployment, CPI, payrolls — the most-queried FRED series. **Without a key: 10 requests/day limit.** Free key registration at `https://www.bls.gov/developers/` gives 500 req/day and 10 years of data per call (vs 3 years without key). ```python import json # Single series GET — no auth needed r = http_get("https://api.bls.gov/publicAPI/v2/timeseries/data/LNS14000000?startyear=2024&endyear=2024") data = json.loads(r) # data['status'] == 'REQUEST_SUCCEEDED' series = data['Results']['series'][0] for point in series['data'][:3]: print(f"{point['year']}-{point['period']} ({point['periodName']}): {point['value']}") # 2024-M12 (December): 4.1 # 2024-M11 (November): 4.2 # 2024-M10 (October): 4.1 ``` ### Multi-series POST (single call, multiple series) ```python import json, urllib.request payload = json.dumps({ "seriesid": ["LNS14000000", "CUSR0000SA0", "CES0000000001"], "startyear": "2023", "endyear": "2024" # "registrationkey": "YOUR_BLS_KEY" # optional: lifts to 500/day, 10yr range }).encode() req = urllib.request.Request( "https://api.bls.gov/publicAPI/v2/timeseries/data/", data=payload, headers={"Content-Type": "application/json"} ) with urllib.request.urlopen(req, timeout=20) as resp: data = json.loads(resp.read().decode()) for s in data['Results']['series']: pts = s['data'] print(f"{s['seriesID']}: {len(pts)} points, latest={pts[0]['value']}") # LNS14000000: 24 points, latest=4.1 (unemployment %) # CUSR0000SA0: 24 points, latest=317.604 (CPI index) # CES0000000001: 24 points, latest=158316 (nonfarm payrolls, thousands) ``` ### Key BLS series (FRED equivalents) | BLS Series ID | FRED Equivalent | Description | |---------------|-----------------|-------------| | `LNS14000000` | `UNRATE` | Unemployment rate, SA (%) | | `CUSR0000SA0` | `CPIAUCSL` | CPI-U All Urban, SA | | `CUUR0000SA0` | `CPIAUCNS` | CPI-U All Urban, not SA | | `CUSR0000SA0L1E` | `CPILFESL` | CPI less food and energy, SA | | `CES0000000001` | `PAYEMS` | Total nonfarm payrolls (thousands) | | `LNS11000000` | `CLF16OV` | Civilian labor force (thousands) | | `LNS12000000` | `CE16OV` | Civilian employment (thousands) | ### BLS rate limits | | Without key | With free key | |--|--|--| | Requests/day | **10** (confirmed: call 11 returns `REQUEST_NOT_PROCESSED`) | 500 | | Series per request | 25 | 50 | | Years per request | 3 | 10 | | Daily or seasonal adjustment | No | Yes | --- ## Option C: World Bank API (no key, unlimited, annual data) Free, no registration, no rate limit observed (10 rapid calls completed in 2.0s). Annual data only — no monthly or quarterly frequency. ```python import json # Single country, single indicator r = http_get("https://api.worldbank.org/v2/country/US/indicator/NY.GDP.MKTP.CD?format=json&per_page=5&mrv=5") data = json.loads(r) page_info = data[0] # {'page': 1, 'pages': 1, 'per_page': 5, 'total': 5, 'lastupdated': '2026-04-08'} items = data[1] # list of observations for item in items: if item['value']: print(f"{item['date']}: ${item['value']/1e12:.2f}T") # 2024: $28.75T # 2023: $27.29T # 2022: $25.60T ``` ### Date range filter and multi-country ```python import json # Historical range: date=YYYY:YYYY r = http_get("https://api.worldbank.org/v2/country/US/indicator/FP.CPI.TOTL.ZG?format=json&date=2015:2024&per_page=15") data = json.loads(r) items = [i for i in data[1] if i['value'] is not None] for item in items: print(f"{item['date']}: {item['value']:.2f}%") # 2024: 2.95% # 2023: 4.12% # 2022: 8.00% # ... # Multi-country: semicolon-separated ISO codes r = http_get("https://api.worldbank.org/v2/country/US;CN;DE;JP;GB/indicator/NY.GDP.MKTP.CD?format=json&date=2023&per_page=10") data = json.loads(r) items = sorted([i for i in data[1] if i['value']], key=lambda x: x['value'], reverse=True) for item in items: print(f"{item['country']['value']}: ${item['value']/1e12:.2f}T") # United States: $27.29T # China: $18.27T # Germany: $4.56T ``` ### Key World Bank indicators (FRED equivalents) | WB Indicator Code | FRED Equivalent | Description | |-------------------|-----------------|-------------| | `NY.GDP.MKTP.CD` | `GDP` | GDP, current USD | | `NY.GDP.MKTP.KD.ZG` | `A191RL1Q225SBEA` | GDP growth rate (%) | | `NY.GDP.PCAP.CD` | `A939RX0Q048SBEA` | GDP per capita (USD) | | `FP.CPI.TOTL.ZG` | `FPCPITOTLZGUSA` | CPI inflation, annual % | | `FP.CPI.TOTL` | `CPIAUCSL` (annual) | CPI level, 2010=100 | | `SL.UEM.TOTL.ZS` | `UNRATE` (annual) | Unemployment rate, ILO model | | `CM.MKT.LCAP.GD.ZS` | — | Stock market cap / GDP ratio | --- ## Option D: Alpha Vantage (free registered key, select indicators) Some economic indicators work with the `demo` key (no registration); most require a free registered key (25 requests/day, instant signup at `https://www.alphavantage.co/support/#api-key`). ```python import json AV_KEY = "demo" # or your registered key # Unemployment rate (works with demo key — confirmed) r = http_get(f"https://www.alphavantage.co/query?function=UNEMPLOYMENT&apikey={AV_KEY}") data = json.loads(r) # data['name'] = 'Unemployment Rate' # data['interval'] = 'monthly' # data['unit'] = 'percent' # data['data'] → list of {date, value}, newest first print(data['data'][0]) # {'date': '2026-03-01', 'value': '4.3'} print(f"Total: {len(data['data'])} months since {data['data'][-1]['date']}") # Total: 939 months since 1948-01-01 ``` ### Which indicators work with demo vs registered key | Function | demo key | Registered key | |----------|----------|----------------| | `UNEMPLOYMENT` | YES | YES | | `INFLATION` | YES (annual) | YES | | `RETAIL_SALES` | YES | YES | | `DURABLES` | YES | YES | | `NONFARM_PAYROLL` | YES | YES | | `REAL_GDP_PER_CAPITA` | YES | YES | | `REAL_GDP` | NO (rate-limited) | YES | | `CPI` | NO (rate-limited) | YES | | `FEDERAL_FUNDS_RATE` | NO (rate-limited) | YES | | `TREASURY_YIELD` | NO (rate-limited) | YES | | `CONSUMER_SENTIMENT` | NO (rate-limited) | YES | ```python import json AV_KEY = "YOUR_FREE_KEY" # from alphavantage.co/support/#api-key # Federal Funds Rate — monthly (requires registered key) r = http_get(f"https://www.alphavantage.co/query?function=FEDERAL_FUNDS_RATE&interval=monthly&apikey={AV_KEY}") data = json.loads(r) for item in data['data'][:3]: print(f"{item['date']}: {item['value']}%") # 2026-03-01: 4.33% # 2026-02-01: 4.33% # 2026-01-01: 4.33% # 10-Year Treasury Yield r = http_get(f"https://www.alphavantage.co/query?function=TREASURY_YIELD&maturity=10year&interval=monthly&apikey={AV_KEY}") data = json.loads(r) print(data['data'][0]) # {'date': '2026-04-17', 'value': '4.34'} ``` --- ## Option E: Browser + CDP (for interactive FRED charts) When you need data from `fred.stlouisfed.org` that has no API equivalent (custom chart combos, release dates visible on page) — or when you have no API key — use the browser. ```python # Navigate to a series page goto_url("https://fred.stlouisfed.org/series/GDP") wait_for_load() # Option 1: Intercept the fredgraph XHR that the chart fires # The page's chart JS calls fredgraph.csv internally — intercept it events = drain_events() # Look for network events with fredgraph.csv in URL # Option 2: Extract the latest value from the page text latest_val = js(""" // The last observation appears in the meta section const el = document.querySelector('.series-meta-observation-end'); el ? el.textContent.trim() : null """) # Option 3: Read the data table if present table_data = js(""" const rows = Array.from(document.querySelectorAll('table.series-observations tr')); rows.map(r => { const cells = r.querySelectorAll('td'); return cells.length >= 2 ? [cells[0].textContent.trim(), cells[1].textContent.trim()] : null; }).filter(Boolean); """) ``` --- ## Rate limits | API | Limit | Notes | |-----|-------|-------| | FRED REST API | 120 req/min | With registered key (free) | | FRED REST API | blocked | Without key — HTTP 400 | | BLS (no key) | 10 req/day | Confirmed: call 11 → `REQUEST_NOT_PROCESSED` | | BLS (with key) | 500 req/day, 50 series/req | Free registration at bls.gov/developers | | World Bank | No limit observed | 10 rapid calls: 2.0s, no 429 | | Alpha Vantage (demo) | 2 req/sec | Demo key rate-limited for most functions | | Alpha Vantage (free key) | 25 req/day | Free at alphavantage.co/support/#api-key | --- ## Gotchas - **fred.stlouisfed.org web endpoints ALL timeout** — The CSV download (`fredgraph.csv`), JSON graph (`fredgraph.json`), text format (`/data/*.txt`), and HTML series pages all hang indefinitely from headless HTTP. This is not a UA or header issue — the server simply does not respond to non-browser connections. Confirmed with multiple UA strings, TCP connect succeeds but no HTTP response is sent. - **FRED API key is mandatory and must be exactly 32 lowercase alphanumeric chars** — "test", "demo", "guest", and keys shorter/longer than 32 chars all return HTTP 400: `"not a 32 character alpha-numeric lower-case string"`. An unregistered 32-char key returns: `"not registered"`. - **Observation values are always strings, not numbers** — The `value` field in FRED observations is always a JSON string: `"4.1"`, not `4.1`. Also `"."` (dot) means missing/not-yet-released. Always check `if o['value'] != '.'` before `float(o['value'])`. - **BLS 10 req/day without key burns fast** — The limit is per-IP per-day. 10 calls is exhausted in one moderate script run. Either register a free BLS key immediately or use World Bank for the same data annually. - **BLS data range: 3 years without key, 10 years with key** — Requesting `startyear=2000&endyear=2024` without a key silently truncates to the most recent 3 years. With a key it returns up to 10 years and includes a `message` field if the range was truncated: `['Year range has been reduced to the system-allowed limit of 10 years.']`. - **World Bank is annual only** — No monthly or quarterly data. For monthly UNRATE or CPI, use BLS. For quarterly GDP, use FRED API or Alpha Vantage `REAL_GDP`. - **World Bank response is a 2-element array** — `data[0]` is pagination metadata, `data[1]` is the observations list. Missing years have `value: null` (not `"."`). Filter with `if item['value'] is not None`. - **Alpha Vantage demo key: 2 req/sec, covers only 6 economic functions** — The other 6 economic functions (`REAL_GDP`, `CPI`, `TREASURY_YIELD`, etc.) return `{"Information": "The demo API key is for demo purposes only..."}`. No error code — just check for the `Information` key in the response. - **FRED `sort_order=desc` returns newest first** — Default is `asc` (oldest first, starting from observation_start). For "get the latest value" use `limit=1&sort_order=desc`. - **FRED series IDs are case-sensitive and exact** — `gdp` returns an error; must be `GDP`. Check `fred.stlouisfed.org/series/{ID}` to verify a series exists before scripting. - **Some FRED series have gaps** — Daily series like `DGS10` and `SP500` skip weekends and holidays. Those dates simply don't appear in the observations array (not represented as `"."`). Weekly and monthly series use the first day of the period as the date (e.g., `2024-01-01` = January 2024). - **FRED `realtime_start`/`realtime_end` in observations** — Every observation has these fields reflecting vintage data. For current data, ignore them. They matter only for "real-time" research (what was the published value on a specific past date).