Blog/The intraday-staleness beat

What ECB-based FX APIs miss between fixes

The ECB publishes its reference fix once per business day around 16:00 CET. Any API sourced only from that fix is stale the moment the market moves after 16:00 — and stays stale until the next day's fix. Here's what that means for a "latest" endpoint, and how intraday-sourced data differs.

ERexchangerate.dev·Jun 19, 2026·5 min read

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.

Key points
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" and market_session so you know exactly what you have.
The data_updated_at field shows when the underlying rate was last written. It is the fastest way to confirm freshness without parsing source.
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.

No signal, not just stale data
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:

TimeECB-only APILive intraday source
Wednesday 16:00 CETECB fix just publishedSpot updating continuously
Wednesday 20:00 UTCWednesday fix (hours stale)Live rate reflecting market move
Thursday 08:00 UTCStill Wednesday fix until ~16:00 CETContinuous live, then Thursday ECB fix at 16:00
Saturday 10:00 UTCFriday fix (stale since Friday ~16:00)Interbank closed; last consensus 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:

python · live call with freshness checkcopy
import requests

API = "https://api.exchangerate.dev/v1"
KEY = "exr_live_..."   # free key at https://exchangerate.dev/signup

resp = requests.get(
    f"{API}/latest/USD",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=10,
)
resp.raise_for_status()
data = resp.json()

print(data["source"])           # live
print(data["market_session"])   # open  (or "weekend" on Sat/Sun)
print(data["data_updated_at"])  # e.g. 2026-06-21T09:14:00Z
print(data["rates"]["EUR"])     # current intraday rate, not the prior fix

If 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_at for 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:

python · staleness guardcopy
from datetime import datetime, timezone, timedelta

data = resp.json()

updated = datetime.fromisoformat(data["data_updated_at"].replace("Z", "+00:00"))
age = datetime.now(timezone.utc) - updated

source = 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")
Indicative, not for settlement
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 consensus from the trading week (interbank FX is closed), labeled market_session: weekend:

python · past weekend datecopy
r = requests.get(
    f"{API}/2026-06-13/USD",   # Saturday
    headers={"Authorization": f"Bearer {KEY}"},
).json()

print(r["source"])              # fred_daily
print(r["market_session"])      # weekend
print(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.

ER
exchangerate.dev
Data beat: the intraday move the ECB fix missed, from our own data.

Keep reading

ReferenceReading source and market_sessionRead GuideECB reference rates, explainedRead GuideHow to backfill FX rates without look-ahead biasRead
More ComparisonsFixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.devCurrencylayer vs ExchangeRate-API
LearnReading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB reference rates, explained
Live RatesEUR/USDGBP/USDUSD/JPY