All articles
Starter Guide

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.

ECB API SDMX exchange rates Eurozone monetary policy Python interest rates SDMX 2.1

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:

📌 Note: The ECB API uses SDMX 2.1 — a standard for statistical data exchange. The key structure is dataflow/key?parameters. The key is a dot-separated combination of dimension values. A . in any position means ‘all values’ for that dimension. For example, EXR/D.USD+GBP.EUR.SP00.A fetches daily USD and GBP rates against EUR.

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.

⚠️ Weekend gap: The ECB’s exchange rate series only publishes on business days. Gaps for weekends and ECB TARGET closing days are expected — do not treat them as missing data errors. Use forward-fill sparingly and always flag filled values in your schema.

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.

Learn

Recent articles

View all →