Call GET /v1/latest/USD during a trading day and you get back source: "live" and market_session: "open", a rate that reflects where the market is right now, not where it was at 16:00 CET yesterday. An API sourced only from the ECB reference fix returns the prior fix with no indication that hours of intraday movement have happened since. The difference can be several tenths of a percent, compounded by news events that land after the fix.
- The ECB publishes its reference fix once per business day around 16:00 CET. Between fixes, the market keeps moving, an ECB-only API does not.
- An API sourced only from the ECB fix returns the prior day's rate for the rest of the trading day and all weekend. The response gives no staleness signal.
- Live-sourced rates update intraday (~60s). The response carries
source: "live"andmarket_sessionso you know exactly what you have. - The
data_updated_atfield shows when the underlying rate was last written. It is the fastest way to confirm freshness without parsingsource. - Intraday moves can be small or large depending on news. A rate that was fixed at 16:00 CET may be off by a significant fraction of a percent by the time you call the API.
How the ECB fix works
The European Central Bank publishes its EXR reference rates once per business day, typically around 16:00 Central European Time. The series goes back to 1999 and is widely used because it is free, official, and consistent. These properties make it a natural upstream source for FX APIs.
The catch: the fix is a single snapshot per business day. Between the 16:00 CET publication and the next day's fix, the market keeps moving, through the afternoon and overnight, and across the weekend the fix sits unchanged from Friday until Monday. An API that reads only from this series has nothing new to show until the next fix, so it returns the last available value regardless of what has happened since.
The problem is not only that the rate is old. Many ECB-only APIs return the prior fix with no market_session, no source, and no data_updated_at field. A caller reading "latest" at 09:00 the next morning has no way to know the number is almost 17 hours old.
The staleness timeline: frozen vs. moving
The table below shows what each type of API returns at different points around a business day. The times are illustrative of the general pattern:
| Time | ECB-only API | Live intraday source |
|---|---|---|
| Wednesday 16:00 CET | ECB fix just published | Spot updating continuously |
| Wednesday 20:00 UTC | Wednesday fix (hours stale) | Live rate reflecting market move |
| Thursday 08:00 UTC | Still Wednesday fix until ~16:00 CET | Continuous live, then Thursday ECB fix at 16:00 |
| Saturday 10:00 UTC | Friday fix (stale since Friday ~16:00) | Interbank closed; last spot rate carried |
The gap is largest first thing in the morning: between the previous day's 16:00 CET fix and the next one, an ECB-only API can be 16–24 hours stale during a full trading day, before accounting for any weekend at all.
Reading a live call with freshness fields
The response from a live-sourced endpoint carries three fields that tell you exactly what you have: source, market_session, and data_updated_at. Here is a call and what to inspect:
import requestsAPI = "https://api.exchangerate.dev/v1"KEY = "exr_live_..." # free key at https://exchangerate.dev/signupresp = requests.get( f"{API}/latest/USD", headers={"Authorization": f"Bearer {KEY}"}, timeout=10,)resp.raise_for_status()data = resp.json()print(data["source"]) # liveprint(data["market_session"]) # open (or "weekend" on Sat/Sun)print(data["data_updated_at"]) # e.g. 2026-06-21T09:14:00Zprint(data["rates"]["EUR"]) # current intraday rate, not the prior fixIf source is "live", the rate is updated intraday (~60s), not pinned to the last daily fix. data_updated_at tells you precisely when. A response with no market_session field and no data_updated_at is a strong indicator of an ECB-only API returning a stale value.
Why it matters for anything reading "latest"
If your code calls /latest and acts on the result, the staleness of an ECB-only response is invisible. You get a 200 status, valid JSON, and a plausible-looking number. There is no error to catch.
- A dashboard that refreshes every 15 minutes will show the same EUR/USD all day until the next ECB fix, even as the real rate moves.
- A cost-reporting job that runs at 08:00 will convert using yesterday's 16:00 fix, missing any overnight move.
- A pricing engine that reads FX rates before invoicing international customers will see the wrong number for most of each trading day.
- Any application that logs
data_updated_atfor auditing will find the ECB-only response gives it nothing useful to log between fixes.
A 200 response with yesterday's number is harder to catch than an error. The data looks right, it's just not current.
Checking staleness in code
The most direct guard is to check data_updated_at against your own clock. If the gap is larger than you expect, more than a few minutes for a live source, or more than 24 hours for a reference fix, something is worth flagging:
from datetime import datetime, timezone, timedeltadata = resp.json()updated = datetime.fromisoformat(data["data_updated_at"].replace("Z", "+00:00"))age = datetime.now(timezone.utc) - updatedsource = data["source"]session = data["market_session"]if source == "ecb_daily": # Reference fix: updated once per business day at ~16:00 CET. # Can be up to 24h stale during a trading day; longer over weekends. print(f"ECB daily fix, age {age}")elif source == "live": print(f"Live rate ({session}), age {age.seconds // 60} minutes")All rates from exchangerate.dev are indicative and published for reference, analytics, and display. The notice field on every response states this. Do not use these rates to settle trades.
Looking up a past weekend date
For historical data, the endpoint GET /v1/{date}/{base} returns a single day. Weekend and holiday dates come back with is_forward_filled: true for reference-rate tiers, meaning no fix was published that day and the prior value was carried forward. Live-tier weekend rows carry the last intraday rate from the trading week (interbank FX is closed), labeled market_session: weekend:
KEY = "YOUR_API_KEY" # Basic or higher for this dater = requests.get( f"{API}/2026-06-13/USD", # Saturday headers={"Authorization": f"Bearer {KEY}"},).json()print(r["source"]) # ecb_dailyprint(r["market_session"]) # weekendprint(r["is_forward_filled"]) # True (no fix was published Saturday)is_forward_filled: true means the value here is the most recent prior publication; nothing was published on the date you asked for. This lets backtests and reporting pipelines treat weekend dates correctly rather than treating a carried-forward value as a fresh observation.