Source on EconIndx: European Central Bank — free, no registration, 139,000+ time series, SDMX 2.1 API.
Access & Pricing
Free with no registration required. The ECB Data Portal API is a public institutional resource — no API key, no rate limit published, no contracts. The base URL is https://data-api.ecb.europa.eu/service. CSV format is the easiest response to parse; SDMX-JSON and XML are also available.
Your First Data Pull
The ECB API uses SDMX 2.1 conventions. A key is built as frequency.currency.denomination.type.variant. The most useful starting point is the exchange rate (EXR) dataflow:
import requests
import pandas as pd
import io
ECB_BASE = "https://data-api.ecb.europa.eu/service"
def fetch_ecb(dataflow: str, key: str, start: str = "2010-01-01") -> pd.DataFrame:
"""Fetch ECB data in CSV format."""
url = f"{ECB_BASE}/data/{dataflow}/{key}"
r = requests.get(url, params={"startPeriod": start, "format": "csvdata"})
r.raise_for_status()
df = pd.read_csv(io.StringIO(r.text))
return df
# Daily EUR/USD exchange rate
eurusd = fetch_ecb("EXR", "D.USD.EUR.SP00.A")
print(f"EUR/USD rows: {len(eurusd)}")
print(f"Columns: {list(eurusd.columns)}")
print(eurusd[["TIME_PERIOD", "OBS_VALUE"]].tail(5))
First Pull: What to Expect
| Dataflow | Key | Description | Rows (from 2010) | Update cadence |
|---|---|---|---|---|
EXR | D.USD.EUR.SP00.A | EUR/USD daily rate | ~3,700 | Business days ~3pm CET |
EXR | D.GBP.EUR.SP00.A | EUR/GBP daily rate | ~3,700 | Business days |
IRS | M.U2.EUR.RT0.IB.L40.YY._X.N | EURIBOR 3M, monthly | ~170 | Monthly |
ICP | M.U2.N.000000.4.INX | HICP, euro area, monthly | ~340 | Monthly |
BSI | M.U2.N.A.A20.A.1.U2.2240.Z01.E | M3, euro area, monthly | ~340 | Monthly |
EUR/USD daily from 1999 (euro introduction) — about 6,500 business days. The CSV response columns always include TIME_PERIOD and OBS_VALUE; other columns are dimension labels.
No data on weekends/holidays: ECB rates are published on TARGET2 business days only. Expect gaps in daily series — handle with forward-fill or explicit null rows, not as missing data.
Key Dataflows to Start With
ECB dataflows are identified by short codes. The most useful for economic pipelines:
Exchange rates (EXR):
D.{CURRENCY}.EUR.SP00.A — daily reference rate
M.{CURRENCY}.EUR.SP00.A — monthly average
Currency codes: USD, GBP, JPY, CHF, CNY, AUD, CAD
Interest rates (IRS):
- Deposit facility rate, ECB key rate:
M.U2.EUR.RT0.IB.L40.YY._X.N - EURIBOR 3M:
M.U2.EUR.RT0.IB.L40.YY._X.N
HICP inflation (ICP):
- Euro area aggregate:
M.U2.N.000000.4.INX - By country: replace
U2with country code (DE,FR,IT,ES, etc.)
Monetary aggregates (BSI):
- M3, euro area:
M.U2.N.A.A20.A.1.U2.2240.Z01.E - M1:
M.U2.N.A.A10.A.1.U2.2240.Z01.E
# Pull multiple currencies at once using + separator
multi_fx = fetch_ecb("EXR", "D.USD+GBP+JPY+CHF.EUR.SP00.A", start="2020-01-01")
print(f"Multi-currency rows: {len(multi_fx)}")
print(multi_fx.groupby("CURRENCY")["OBS_VALUE"].describe())
Data Tolerance & Validation
What’s normal:
- EUR reference rates are mid-market (not bid/ask). They differ from interbank transaction rates — don’t use them for trade pricing, use them for accounting and analytics.
- Gaps on TARGET2 holidays (not just weekends — also ECB-specific holidays). A gap of 1–4 days is normal; longer gaps indicate a data problem.
- M3 and BSI data revised monthly for the prior 2–3 months. HICP revised occasionally with annual benchmark.
- The
OBS_STATUScolumn carries quality flags:A= normal,E= estimated,P= provisional. Filter or store accordingly.
Validation checks:
def validate_ecb_pull(df: pd.DataFrame, series_name: str) -> dict:
df["TIME_PERIOD"] = pd.to_datetime(df["TIME_PERIOD"], errors="coerce")
latest = df["TIME_PERIOD"].max()
days_stale = (pd.Timestamp.today() - latest).days
# Check for suspiciously large moves (>5% daily for FX is extremely rare)
df_sorted = df.sort_values("TIME_PERIOD")
df_sorted["pct_chg"] = df_sorted["OBS_VALUE"].pct_change().abs()
outliers = (df_sorted["pct_chg"] > 0.05).sum()
return {
"series": series_name,
"row_count": len(df),
"latest_date": str(latest.date()),
"days_stale": days_stale,
"stale_alert": days_stale > 5, # daily series
"outlier_moves": int(outliers), # large daily moves worth investigating
}
report = validate_ecb_pull(eurusd, "EUR/USD")
print(report)
# Expected: days_stale=0-3 (weekend gap), outlier_moves < 5 (crisis periods only)
Alert thresholds:
- Daily FX series: alert if more than 5 business days stale
- Monthly monetary data: alert if more than 45 days stale
- Any OBS_VALUE that is more than 15% different from prior period for an FX rate: investigate
Spot-Check Against Known Values
A useful sanity check — ECB publishes its reference rates on the website. Your pulled EUR/USD for a known business day should match to 4 decimal places:
# Spot-check: ECB published EUR/USD on 2024-01-02 = 1.0963
known_date = "2024-01-02"
pulled_value = eurusd[eurusd["TIME_PERIOD"] == known_date]["OBS_VALUE"].values
if len(pulled_value):
diff = abs(pulled_value[0] - 1.0963)
print(f"EUR/USD {known_date}: pulled={pulled_value[0]}, expected≈1.0963, diff={diff:.6f}")
assert diff < 0.0001, "Reference rate mismatch — check API response"
Schema Stability
ECB SDMX dataflow codes are very stable. The CSV column names (FREQ, CURRENCY, TIME_PERIOD, OBS_VALUE, etc.) have not changed. The main risk is dimension value changes when new ECB member countries join — aggregates like U2 (euro area) are extended, which can cause a small break in aggregate-level series. Monitor the series metadata for composition changes.
Next Steps
- Full details on all dataflows, rate limits, and SDMX-JSON format at the ECB source page on EconIndx
- Browse all 139,000+ series at data.ecb.europa.eu
- Python:
pip install ecbfor a thin wrapper, orpip install pandasdmxfor full SDMX support