Frankfurter API for Currency Conversion in Python
Languages: Python, curl · Estimated time: ~5 minutes · Frankfurter live status ↗
Frankfurter serves European Central Bank exchange rates with no key, no rate limit, and a clean REST interface. This tutorial covers fetching latest rates, converting amounts, and querying historical rates.
Frankfurter is the best free currency API for most use cases: no key, no credit card, unlimited calls, ECB data. It's been running reliably for years. The live status is at /frankfurter.
The ECB only publishes rates on business days. On weekends and EU bank holidays, Frankfurter returns the most recent available rate — this is the correct behaviour, not a bug.
Basic Requests with curl
# Latest rates (all currencies vs EUR base)
curl https://api.frankfurter.app/latest
# Latest rates vs USD base
curl "https://api.frankfurter.app/latest?from=USD"
# Convert 100 USD to GBP and EUR
curl "https://api.frankfurter.app/latest?amount=100&from=USD&to=GBP,EUR"
# { "amount": 100.0, "base": "USD", "date": "2026-05-14",
# "rates": { "GBP": 79.23, "EUR": 91.88 } }
# Historical rate on a specific date
curl "https://api.frankfurter.app/2024-01-15?from=USD&to=EUR"
# All supported currencies
curl https://api.frankfurter.app/currencies Python: Currency Conversion Class
import requests
from datetime import date, timedelta
from functools import lru_cache
FRANKFURTER_BASE = "https://api.frankfurter.app"
@lru_cache(maxsize=100)
def get_rates(base: str = "EUR", on_date: str = "latest") -> dict:
"""
Fetch exchange rates. Results are cached (rates change once per day at most).
"""
url = f"{FRANKFURTER_BASE}/{on_date}"
resp = requests.get(url, params={"from": base}, timeout=10)
resp.raise_for_status()
return resp.json()
def convert(amount: float, from_ccy: str, to_ccy: str) -> float:
"""
Convert an amount from one currency to another.
Uses today's ECB rate (updated once per business day).
"""
if from_ccy == to_ccy:
return amount
data = get_rates(base=from_ccy)
rate = data["rates"].get(to_ccy)
if rate is None:
raise ValueError(f"Currency '{to_ccy}' not supported. Check /currencies endpoint.")
return round(amount * rate, 2)
def get_historical_rate(from_ccy: str, to_ccy: str, on_date: date) -> float:
"""
Get the ECB rate for a specific date.
Note: weekends/holidays return the most recent business day's rate.
"""
data = get_rates(base=from_ccy, on_date=on_date.isoformat())
return data["rates"][to_ccy]
# Examples
print(convert(100, "USD", "EUR")) # e.g. 91.88
print(convert(500, "GBP", "JPY")) # e.g. 94210.50
# Historical: what was 1 USD in EUR on 2024-01-01?
rate = get_historical_rate("USD", "EUR", date(2024, 1, 2)) # Jan 1 is a holiday
print(f"1 USD = {rate} EUR on 2024-01-02") Handling the ECB Calendar
The ECB doesn't publish rates on Saturdays, Sundays, or EU bank holidays (New Year's, Easter, Christmas). Calling Frankfurter for `2026-01-01` returns rates dated `2025-12-31` (the last business day). The `date` field in the response tells you which date the rates are from.
If your application displays the rate date to users (e.g. 'rate as of [date]'), use `data['date']` rather than the date you requested.
# Getting the actual rate date vs the requested date
resp = requests.get("https://api.frankfurter.app/2026-01-01?from=USD&to=EUR")
data = resp.json()
print(f"Requested: 2026-01-01, Rate date: {data['date']}")
# Rate date will be 2025-12-31 (New Year's Day is a holiday)