The shortest path to exchange rates in Python is a GET request to /v1/latest/USD. You get back JSON with per-currency sources and effective_at metadata, so the code can retain the context around each value. Your first call can be anonymous.
- One GET to
/v1/latest/{base}returns rates for 144 currencies as JSON. - Use
sources[currency]andeffective_at[currency]to describe the displayed currency;market_sessiondescribes the trading session. - The free tier is 5,000 calls a month at 10 requests a minute, no card required.
- Rates are indicative, published for reference and analytics, not as a dealing quote.
Your first call, no dependencies
You need nothing beyond the standard library. urllib.request handles a single call. To build a complete threshold monitor with saved state, see exchange-rate alerts in Python. Start with this request:
import jsonfrom decimal import Decimalimport urllib.requesturl = "https://api.exchangerate.dev/v1/latest/USD?symbols=EUR,GBP"with urllib.request.urlopen(url, timeout=10) as response: data = json.load(response, parse_float=Decimal)print(data["rates"]["EUR"])print(data["sources"]["EUR"], data["effective_at"]["EUR"])print(data["market_session"])That runs without a key. Anonymous calls are capped per IP address, so for anything past a quick test you want a free key.
With requests and an API key
Most projects already use requests. Install requests, set EXCHANGERATE_API_KEY in your server environment, and pass the key as a bearer token:
# Install first: python -m pip install requestsimport osfrom decimal import Decimalimport requestsresponse = requests.get( "https://api.exchangerate.dev/v1/latest/USD", headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"}, timeout=10, allow_redirects=False,)if response.status_code == 429: raise RuntimeError(f"Rate limited; Retry-After: {response.headers.get('Retry-After')}")if 300 <= response.status_code < 400: raise RuntimeError("Unexpected redirect")response.raise_for_status()data = response.json(parse_float=Decimal)print(data["rates"]["GBP"], data["effective_at"]["GBP"])A free key gives your application an account-level quota. Send it as a bearer token; the X-API-Key header is also accepted by platforms that cannot set Authorization.
Convert an amount
To convert rather than list rates, call /v1/convert. It returns the rate and the converted amount together:
import osfrom decimal import Decimalimport requestsresponse = requests.get( "https://api.exchangerate.dev/v1/convert/USD/EUR/100", headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"}, timeout=10, allow_redirects=False,)if not 200 <= response.status_code < 300: raise RuntimeError(f"HTTP {response.status_code}; Retry-After: {response.headers.get('Retry-After')}")data = response.json(parse_float=Decimal)print(data["rate"], data["converted"])If you already use forex-python
The forex-python documentation shows CurrencyRates().get_rate("USD", "EUR") and convert() for its configured source. Installing that package does not connect it to exchangerate.dev. To use this API, call its HTTP endpoint directly with requests or the standard library, as above.
For money calculations, parse decimal values with Decimal and preserve the original rate. Choose rounding rules for the target currency at the final amount boundary. A Python library’s presence does not tell you how recent its upstream observation is; check the response’s source and observation fields.
Know how fresh a rate is
For a displayed currency, read sources[currency] and effective_at[currency]. The aggregate source field describes the least-fresh contributing data. source: live (spot), ecb_daily (ECB fix), market_daily (stored daily market-feed close for currencies without an ECB fix). market_session is open, weekend, or interbank_closed for a known non-weekend closure.
| Field value | What it means | When it moves |
|---|---|---|
| source: live | Indicative spot rate | Intraday (~60s), trading week |
| source: ecb_daily | Official reference fix | Once per business day |
| source: market_daily | Stored close, no ECB fix | Daily market feed |
| market_session: weekend | Saturday or Sunday (interbank closed) | Reference fix carries Friday's value; live feed not updating |
Rates are published for reference, analytics, and display. They are not a dealing quote and should not be used to settle a trade. Every response carries this in its notice field.
Read a single past day
Put a past date in the path. This example needs Basic or above. Plus and Pro reach 1999 where the pair has data. The response adds is_forward_filled when it uses an earlier observation for the requested day, such as a weekend or holiday:
import osfrom decimal import Decimalimport requestsresponse = requests.get( "https://api.exchangerate.dev/v1/2026-03-14/USD?symbols=JPY", headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"}, timeout=10, allow_redirects=False,)if not 200 <= response.status_code < 300: raise RuntimeError(f"HTTP {response.status_code}; Retry-After: {response.headers.get('Retry-After')}")data = response.json(parse_float=Decimal)print(data["rates"]["JPY"], data["is_forward_filled"])For a full series across a date range, use /v1/range with start_date and end_date. The historical time-series guide shows pagination and the 366-row JSON page limit.