Guide/Time series
Guide

Historical FX rates and time series in one call

Pull a multi-currency FX series across a date range with a single request to /v1/range. Daily history back to 1999, returned as a data array of daily rows ready to load into pandas.

MMexchangerate.dev·Jun 19, 2026·6 min read

One call to /v1/range returns a data array with one row per published day in your window, for as many currencies as you pass in symbols. No looping required. Each row carries its own date, rates, source, and is_forward_filled.

Key points
GET /v1/range returns a data array of daily rows in one call, no looping over single-day endpoints.
Params are start_date and end_date (snake_case). symbols is a comma-separated string.
Each row carries source and is_forward_filled. Reference sources publish on business days, so weekend dates are not returned as rows.
Daily history is available back to 1999.
Rates are indicative, published for reference and analytics.

The range endpoint

GET /v1/range takes four query parameters: base, symbols, start_date, and end_date. All four are required. symbols is a comma-separated list of currency codes. The response has a data key: an array of daily rows, plus has_more and next_cursor for paging through long ranges.

curl · four-day USD seriescopy
$ curl -G "https://api.exchangerate.dev/v1/range" \
    -H "Authorization: Bearer exr_live_..." \
    --data-urlencode "base=USD" \
    --data-urlencode "symbols=EUR,GBP" \
    --data-urlencode "start_date=2026-06-10" \
    --data-urlencode "end_date=2026-06-13"

Each element of data is a self-contained daily row with its own date, rates, source, and is_forward_filled:

json · response (abbreviated)copy
{
  "result": "success",
  "base": "USD",
  "start_date": "2026-06-10",
  "end_date": "2026-06-13",
  "data": [
    { "date": "2026-06-10", "rates": { "EUR": 0.86663, "GBP": 0.74727 },
      "source": "ecb_daily", "is_forward_filled": false },
    { "date": "2026-06-11", "rates": { "EUR": 0.86678, "GBP": 0.74829 },
      "source": "ecb_daily", "is_forward_filled": false },
    { "date": "2026-06-12", "rates": { "EUR": 0.86453, "GBP": 0.74613 },
      "source": "ecb_daily", "is_forward_filled": false }
  ],
  "has_more": false,
  "next_cursor": null
}

Note that the Saturday (2026-06-13) is absent. Reference sources publish on business days only, so the range returns the published days in the window rather than one row per calendar day.

Python example

The same request in Python with requests. Iterate the data array, reading each row by key:

python · multi-currency time seriescopy
import requests

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

params = {
    "base": "USD",
    "symbols": "EUR,GBP",
    "start_date": "2026-06-10",
    "end_date": "2026-06-13",
}

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

for row in data["data"]:
    filled = row["is_forward_filled"]
    print(row["date"], row["rates"]["EUR"], row["rates"]["GBP"], "filled" if filled else "")

The is_forward_filled flag

Every row carries is_forward_filled. For a normal business day it is false: that day had a published fix. It is true only when a row repeats the prior value because no fix was published on a day the range still includes, such as a recognised public holiday. Weekends are simply not present as rows.

Need a specific weekend date?
The range returns published days, so a Saturday will not appear in data. To get the carried-forward value for one specific weekend or holiday date, request it directly with /v1/{date}/{base}, which returns the prior value and sets is_forward_filled: true.

/v1/range vs looping /v1/{date}/{base}

For a single day, /v1/{date}/{base} is the right call: it is simple, cacheable, and carries is_forward_filled at the response root. For anything spanning more than one date, /v1/range returns the whole series in one round trip.

Approach/v1/{date}/{base} loop/v1/range
HTTP requests for a 30-day seriesUp to 30 requests1 request (plus paging)
Rate-limit cost (30 req/min free tier)Can use a full minuteNegligible
Response shapeOne day per responseA data array of days
Forward-fill flagRoot-level fieldPer-row field in data
Best forOne-off date lookupsCharting, backtesting, bulk export

History back to 1999

The ECB EXR series and FRED daily data go back to 1999 for many pairs. You can pass any start_date in that range. The source field on each row tells you which dataset powered it: ecb_daily, fred_daily, or live.

Loading into pandas

The data array loads into a DataFrame with pd.json_normalize, which flattens the nested rates object into rates.EUR, rates.GBP, and so on:

python · load into pandascopy
import pandas as pd

# data["data"] is the list of daily rows from the API
df = pd.json_normalize(data["data"])
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index()

# keep only rows that were actually published that day
df = df[~df["is_forward_filled"]]

print(df[["rates.EUR", "rates.GBP"]].tail())
pandas is not required
The code above is one convenience pattern. The API returns plain JSON, so any language or tool that can parse JSON works the same way.
MM
exchangerate.dev
Integration guides for developers working with FX data.

Keep reading

TutorialHow to get exchange rates in PythonRead GuideHow to backfill FX rates without look-ahead biasRead ReferenceReading source and market_sessionRead