All articles

Getting Started with the ECB Data Portal API

First pull, key dataflows, and what to expect when building pipelines on the European Central Bank SDMX 2.1 API.

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

DataflowKeyDescriptionRows (from 2010)Update cadence
EXRD.USD.EUR.SP00.AEUR/USD daily rate~3,700Business days ~3pm CET
EXRD.GBP.EUR.SP00.AEUR/GBP daily rate~3,700Business days
IRSM.U2.EUR.RT0.IB.L40.YY._X.NEURIBOR 3M, monthly~170Monthly
ICPM.U2.N.000000.4.INXHICP, euro area, monthly~340Monthly
BSIM.U2.N.A.A20.A.1.U2.2240.Z01.EM3, euro area, monthly~340Monthly

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 U2 with 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_STATUS column 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 ecb for a thin wrapper, or pip install pandasdmx for full SDMX support

Learn

Recent guides

View all →